Body
This commit is contained in:
@@ -258,12 +258,19 @@ def sync_wellness(garmin, user_id: int, since: Optional[datetime], db,
|
||||
row["body_battery"] = _json.dumps(bb)
|
||||
|
||||
# Intraday heart rate — store non-null [epoch_ms, bpm] pairs
|
||||
intraday = None
|
||||
if hr_raw:
|
||||
raw_vals = hr_raw.get("heartRateValues") or []
|
||||
intraday = [[int(ts), int(v)] for ts, v in raw_vals if v is not None]
|
||||
if intraday:
|
||||
row["intraday_hr"] = intraday
|
||||
|
||||
# High-resolution body battery derived from BB checkpoints + intraday HR
|
||||
if bb and intraday:
|
||||
hires = _compute_body_battery_hires(bb.get("values") or [], intraday)
|
||||
if hires:
|
||||
row["body_battery_hires"] = _json.dumps(hires)
|
||||
|
||||
if not row:
|
||||
continue
|
||||
|
||||
@@ -351,6 +358,72 @@ def _parse_body_battery(bb_response, day_str: str):
|
||||
}
|
||||
|
||||
|
||||
def _compute_body_battery_hires(bb_values, intraday_hr):
|
||||
"""
|
||||
Produce a higher-resolution body battery series by interpolating between
|
||||
sparse BB checkpoints using intraday HR as a proxy for effort.
|
||||
|
||||
During drain segments (BB falling) the drain is distributed proportionally
|
||||
to how much each HR reading exceeds the day's median — peaks spend battery
|
||||
faster than valleys. During recovery segments (BB rising) recovery is
|
||||
spread uniformly over time.
|
||||
|
||||
Returns [[ts_ms, level], ...] at the granularity of intraday HR, or None
|
||||
if inputs are insufficient.
|
||||
"""
|
||||
if not bb_values or not intraday_hr or len(bb_values) < 2:
|
||||
return None
|
||||
|
||||
bb = sorted(bb_values, key=lambda x: x[0])
|
||||
hr = sorted(intraday_hr, key=lambda x: x[0])
|
||||
|
||||
hr_vals = [bpm for _, bpm in hr if bpm is not None and bpm > 0]
|
||||
if not hr_vals:
|
||||
return None
|
||||
|
||||
hr_median = sorted(hr_vals)[len(hr_vals) // 2]
|
||||
|
||||
result = []
|
||||
for i in range(len(bb) - 1):
|
||||
t1, L1 = bb[i][0], bb[i][1]
|
||||
t2, L2 = bb[i + 1][0], bb[i + 1][1]
|
||||
delta = L2 - L1
|
||||
|
||||
seg_hr = [(ts, bpm) for ts, bpm in hr if t1 <= ts <= t2 and bpm is not None]
|
||||
result.append([t1, round(float(L1), 1)])
|
||||
|
||||
if not seg_hr or abs(delta) < 1:
|
||||
continue
|
||||
|
||||
if delta < 0:
|
||||
# Drain: weight each reading by HR above median
|
||||
efforts = [max(0.0, bpm - hr_median) for _, bpm in seg_hr]
|
||||
total = sum(efforts) or 1.0
|
||||
cumul = 0.0
|
||||
for j, (ts, bpm) in enumerate(seg_hr):
|
||||
cumul += efforts[j] * delta / total
|
||||
level = max(0.0, min(100.0, L1 + cumul))
|
||||
result.append([ts, round(level, 1)])
|
||||
else:
|
||||
# Recovery: linear over time
|
||||
span = max(1, t2 - t1)
|
||||
for ts, _ in seg_hr:
|
||||
frac = (ts - t1) / span
|
||||
level = max(0.0, min(100.0, L1 + delta * frac))
|
||||
result.append([ts, round(level, 1)])
|
||||
|
||||
result.append([bb[-1][0], round(float(bb[-1][1]), 1)])
|
||||
|
||||
# Deduplicate and sort
|
||||
seen, out = set(), []
|
||||
for item in sorted(result, key=lambda x: x[0]):
|
||||
if item[0] not in seen:
|
||||
seen.add(item[0])
|
||||
out.append(item)
|
||||
|
||||
return out if len(out) > 4 else None
|
||||
|
||||
|
||||
def _safe(fn, *args):
|
||||
try:
|
||||
return fn(*args)
|
||||
|
||||
Reference in New Issue
Block a user