Fix sleep score parsing, dashboard body battery, segment direction
- Garmin sync: read sleepScores from dailySleepDTO (Garmin nests it there), so sleep score is actually stored instead of always null - Dashboard: pass YYYY-MM-DD to the intraday endpoint (was a full ISO timestamp), so the body-battery tile populates - Segment matching: follow the segment in its created direction with a path-length sanity check, so out-and-back routes no longer match an early start pass to a late finish (the >1h bogus segment times) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -555,8 +555,10 @@ def _parse_day(stats, sleep_data, hrv_data) -> dict:
|
||||
if spo2 and 50 < float(spo2) <= 100:
|
||||
row["spo2_avg"] = float(spo2)
|
||||
|
||||
# Sleep score — structure varies across firmware
|
||||
scores = sleep_data.get("sleepScores") or sleep_data.get("sleepScore")
|
||||
# Sleep score — Garmin nests it under dailySleepDTO.sleepScores on most
|
||||
# firmware, but some return it at the top level; check both.
|
||||
scores = (dto.get("sleepScores") or sleep_data.get("sleepScores")
|
||||
or dto.get("sleepScore") or sleep_data.get("sleepScore"))
|
||||
if isinstance(scores, dict):
|
||||
overall = scores.get("overall") or scores.get("qualityScore")
|
||||
if isinstance(overall, dict):
|
||||
|
||||
@@ -102,49 +102,65 @@ def match_segment_in_activity(
|
||||
tol_m: float = 30.0,
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
Determine whether an activity track traverses a segment's GPS geometry, and if so
|
||||
how long it took. Works even when the activity's overall route differs — only the
|
||||
overlapping stretch matters.
|
||||
Determine whether an activity track traverses a segment's GPS geometry in the
|
||||
segment's own direction, and if so how long the fastest such traversal took.
|
||||
Works even when the activity's overall route differs — only the overlapping
|
||||
stretch matters.
|
||||
|
||||
seg_coords: [(lat, lon), ...] segment geometry (start → end).
|
||||
act_coords: [(lat, lon), ...] activity track, in time order.
|
||||
act_times: parallel list of datetimes for act_coords.
|
||||
|
||||
Strategy: anchor on the activity point nearest the segment start, then the nearest
|
||||
point (at/after it) to the segment end, then verify a few intermediate segment
|
||||
points are each passed within tolerance between those anchors. Returns the time
|
||||
between the start and end anchors, or None if the activity doesn't follow the segment.
|
||||
Strategy: for every pass of the activity near the segment START, walk forward
|
||||
accumulating path length; accept the traversal only if the activity reaches the
|
||||
segment END after covering roughly the segment's own length (so an out-and-back
|
||||
route can't match an early start to a late finish), and the intermediate segment
|
||||
points are passed in order. Returns the shortest valid traversal time, or None.
|
||||
"""
|
||||
n = len(act_coords)
|
||||
if n < 2 or len(seg_coords) < 2:
|
||||
m = len(seg_coords)
|
||||
if n < 2 or m < 2:
|
||||
return None
|
||||
|
||||
start_pt, end_pt = seg_coords[0], seg_coords[-1]
|
||||
|
||||
si, sd = None, tol_m
|
||||
for i in range(n):
|
||||
d = haversine_m(act_coords[i], start_pt)
|
||||
if d < sd:
|
||||
sd, si = d, i
|
||||
if si is None:
|
||||
seg_len = sum(haversine_m(seg_coords[k], seg_coords[k + 1]) for k in range(m - 1))
|
||||
if seg_len <= 0:
|
||||
return None
|
||||
|
||||
ei, ed = None, tol_m
|
||||
near_start = lambda i: haversine_m(act_coords[i], start_pt) <= tol_m
|
||||
|
||||
# One candidate entry per pass through the start region (first point of each run).
|
||||
entries = [i for i in range(n) if near_start(i) and (i == 0 or not near_start(i - 1))]
|
||||
|
||||
best = None
|
||||
for si in entries:
|
||||
path = 0.0
|
||||
ei = None
|
||||
for i in range(si + 1, n):
|
||||
d = haversine_m(act_coords[i], end_pt)
|
||||
if d < ed:
|
||||
ed, ei = d, i
|
||||
if ei is None or ei <= si:
|
||||
return None
|
||||
path += haversine_m(act_coords[i - 1], act_coords[i])
|
||||
if path > seg_len * 1.5: # wandered too far without finishing → wrong pass/direction
|
||||
break
|
||||
if path >= seg_len * 0.6 and haversine_m(act_coords[i], end_pt) <= tol_m:
|
||||
ei = i
|
||||
break
|
||||
if ei is None:
|
||||
continue
|
||||
|
||||
# Verify the activity actually follows the segment shape between the anchors.
|
||||
# Confirm the activity follows the segment shape in order between the anchors.
|
||||
ok = True
|
||||
for frac in (0.25, 0.5, 0.75):
|
||||
sp = seg_coords[int(frac * (len(seg_coords) - 1))]
|
||||
if not any(haversine_m(act_coords[i], sp) <= tol_m for i in range(si, ei + 1)):
|
||||
return None
|
||||
sp = seg_coords[int(frac * (m - 1))]
|
||||
if not any(haversine_m(act_coords[k], sp) <= tol_m for k in range(si, ei + 1)):
|
||||
ok = False
|
||||
break
|
||||
if not ok:
|
||||
continue
|
||||
|
||||
dur = (act_times[ei] - act_times[si]).total_seconds()
|
||||
return dur if dur > 0 else None
|
||||
if dur > 0 and (best is None or dur < best):
|
||||
best = dur
|
||||
|
||||
return best
|
||||
|
||||
|
||||
def find_best_split_time(
|
||||
|
||||
@@ -152,7 +152,7 @@ export default function DashboardPage() {
|
||||
const rows = [...(recentHealth || [])].sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
const pick = f => rows.find(d => d[f] != null)?.[f] ?? null
|
||||
return {
|
||||
date: rows[0]?.date ?? null,
|
||||
date: rows[0]?.date ? rows[0].date.slice(0, 10) : null, // intraday endpoint wants YYYY-MM-DD
|
||||
resting_hr: pick('resting_hr'),
|
||||
sleep_duration_s: pick('sleep_duration_s'),
|
||||
hrv_nightly_avg: pick('hrv_nightly_avg'),
|
||||
|
||||
Reference in New Issue
Block a user