mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
feat(whoop): map cycle average/max HR and sleep disturbance count
mapCycle now emits AVERAGE_HEART_RATE and MAX_HEART_RATE from the cycle score's average_heart_rate / max_heart_rate — both were already on the typed WhoopCycle interface but dropped on the floor. mapSleep now reads stage_summary.disturbance_count (added to the WhoopSleep interface) and emits SLEEP_DISTURBANCE_COUNT. All three guard nulls / unscored records and carry distinct field tags (avg_hr / max_hr / disturbances) so they don't collide with the existing strain / energy / stage rows under the shared resource externalId. Update mapping.md to document the three and note the cycle HR pair were previously fetched-but-dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ef188e0503
commit
dabedd2edc
2 files changed
+47
-4
No files matched your search
+41
-3
@@ -190,6 +190,9 @@ export interface WhoopSleep {
|
||||
total_light_sleep_time_milli: number;
|
||||
total_slow_wave_sleep_time_milli: number;
|
||||
total_rem_sleep_time_milli: number;
|
||||
// v1.12.8 — per-night sleep disturbance tally. Optional: older / still-
|
||||
// scoring records may omit it.
|
||||
disturbance_count?: number;
|
||||
};
|
||||
sleep_needed: {
|
||||
baseline_milli: number;
|
||||
@@ -581,18 +584,29 @@ export function mapSleep(s: WhoopSleep): MappedMeasurement[] {
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof stages.disturbance_count === "number") {
|
||||
out.push({
|
||||
type: "SLEEP_DISTURBANCE_COUNT",
|
||||
value: stages.disturbance_count,
|
||||
unit: "count",
|
||||
measuredAt,
|
||||
fieldTag: "disturbances",
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map one WHOOP cycle (day) record: `DAY_STRAIN` (distinct from the COMPUTED
|
||||
* `STRAIN_SCORE`) and `ENERGY_EXPENDITURE_KJ` (kept in native kJ). Energy is
|
||||
* NOT converted to kcal here — that conversion is for the workout path only.
|
||||
* `STRAIN_SCORE`), `ENERGY_EXPENDITURE_KJ` (kept in native kJ), and the day's
|
||||
* whole-cycle `AVERAGE_HEART_RATE` / `MAX_HEART_RATE`. Energy is NOT converted
|
||||
* to kcal here — that conversion is for the workout path only.
|
||||
*/
|
||||
export function mapCycle(c: WhoopCycle): MappedMeasurement[] {
|
||||
if (!c.score) return [];
|
||||
const measuredAt = new Date(c.start);
|
||||
return [
|
||||
const out: MappedMeasurement[] = [
|
||||
{
|
||||
type: "DAY_STRAIN",
|
||||
value: round2(c.score.strain),
|
||||
@@ -608,6 +622,30 @@ export function mapCycle(c: WhoopCycle): MappedMeasurement[] {
|
||||
fieldTag: "energy_kj",
|
||||
},
|
||||
];
|
||||
|
||||
// v1.12.8 — the cycle score's average / max heart rate were fetched but
|
||||
// dropped before this release. Distinct fieldTags keep them from colliding
|
||||
// with the strain / energy rows under the shared cycle `externalId`.
|
||||
if (typeof c.score.average_heart_rate === "number") {
|
||||
out.push({
|
||||
type: "AVERAGE_HEART_RATE",
|
||||
value: Math.round(c.score.average_heart_rate),
|
||||
unit: "bpm",
|
||||
measuredAt,
|
||||
fieldTag: "avg_hr",
|
||||
});
|
||||
}
|
||||
if (typeof c.score.max_heart_rate === "number") {
|
||||
out.push({
|
||||
type: "MAX_HEART_RATE",
|
||||
value: Math.round(c.score.max_heart_rate),
|
||||
unit: "bpm",
|
||||
measuredAt,
|
||||
fieldTag: "max_hr",
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Metres → centimetres (WHOOP `height_meter` → `User.heightCm`). */
|
||||
|
||||
@@ -35,9 +35,12 @@ A recovery record with `score === null` (WHOOP still scoring) maps to nothing.
|
||||
| `sleep_efficiency_percentage` | `SLEEP_EFFICIENCY` | % | `sleep_eff` | — |
|
||||
| `sleep_consistency_percentage` | `SLEEP_CONSISTENCY` | % | `sleep_consistency` | — |
|
||||
| `respiratory_rate` | `RESPIRATORY_RATE` | breaths/min | `resp_rate` | — |
|
||||
| `stage_summary.disturbance_count` | `SLEEP_DISTURBANCE_COUNT` | count | `disturbances` | — |
|
||||
|
||||
Stage durations are ms→minutes (`÷ 60000`). One row per stage per night
|
||||
(same pattern as Apple Health). Percentage / respiratory fields are optional.
|
||||
(same pattern as Apple Health). Percentage / respiratory / disturbance-count
|
||||
fields are optional. `disturbance_count` (v1.12.8) is a per-night tally — a
|
||||
plain integer, stored as-is.
|
||||
|
||||
## Cycle (`/v2/cycle`) — record `start` is `measuredAt`
|
||||
|
||||
@@ -45,6 +48,8 @@ Stage durations are ms→minutes (`÷ 60000`). One row per stage per night
|
||||
|---|---|---|---|---|
|
||||
| `cycle.score.strain` | `DAY_STRAIN` | score | `day_strain` | 0–21 WHOOP scale. Distinct from the COMPUTED `STRAIN_SCORE` (0–100 TRIMP proxy). |
|
||||
| `cycle.score.kilojoule` | `ENERGY_EXPENDITURE_KJ` | kJ | `energy_kj` | Native kJ — NOT converted to kcal (the workout path converts). |
|
||||
| `cycle.score.average_heart_rate` | `AVERAGE_HEART_RATE` | bpm | `avg_hr` | v1.12.8 — the day's whole-cycle average HR. Fetched but dropped before v1.12.8; now mapped. Distinct from spot `PULSE` / `RESTING_HEART_RATE` / `WALKING_HEART_RATE_AVERAGE`. |
|
||||
| `cycle.score.max_heart_rate` | `MAX_HEART_RATE` | bpm | `max_hr` | v1.12.8 — the day's whole-cycle peak HR. Fetched but dropped before v1.12.8; now mapped. |
|
||||
|
||||
## Workout (`/v2/activity/workout`) — into `Workout`, not `Measurement`
|
||||
|
||||
|
||||
Reference in new issue
Block a user