mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Oura's current authorization server enforces scopes per collection. daily_spo2 needs spo2; vO2_max, daily_cardiovascular_age and daily_resilience need heart_health. The authorize request now asks for both. A grant issued before keeps answering 401 on those four collections while every other one succeeds. Such a 401 next to a success is now recorded as a scope gap rather than a failure: it no longer counts toward the reconnect threshold, it no longer forces a token refresh once known, and the Oura card asks for one reconnect instead. The Withings card names the readings the account's API plan withholds.
93 lines
2.4 KiB
TypeScript
93 lines
2.4 KiB
TypeScript
/**
|
|
* The readings a Withings API-plan refusal can withhold, as the settings card
|
|
* names them. Pure: no database, no network, so the OpenAPI registry and the
|
|
* client bundle can import it.
|
|
*/
|
|
import type { WithingsApiPlanState } from "./api-plan";
|
|
|
|
/** The Withings plans page, linked from the settings card and the docs. */
|
|
export const WITHINGS_API_PLANS_URL =
|
|
"https://static.withings.com/content/api/plans/plans.html";
|
|
|
|
/**
|
|
* The closed set of reading names the Withings card can show for a plan
|
|
* refusal, in display order. Server-resolved so every client renders the same
|
|
* list; each has a label under `settings.integrations.withings.plan.items`.
|
|
*/
|
|
export const WITHINGS_PLAN_FEATURES = [
|
|
"spo2",
|
|
"vascularAge",
|
|
"pulseWaveVelocity",
|
|
"vo2Max",
|
|
"visceralFat",
|
|
"skinTemperature",
|
|
"ecg",
|
|
"sleepScore",
|
|
"sleepHrv",
|
|
"sleepHeartRate",
|
|
"sleepRespiratoryRate",
|
|
"sleepSpo2",
|
|
"bodyTemperature",
|
|
"bodyFat",
|
|
"fatMass",
|
|
"fatFreeMass",
|
|
"muscleMass",
|
|
"bodyWater",
|
|
"boneMass",
|
|
"heartRate",
|
|
"bloodPressure",
|
|
"weight",
|
|
] as const;
|
|
|
|
export type WithingsPlanFeature = (typeof WITHINGS_PLAN_FEATURES)[number];
|
|
|
|
const MEASTYPE_FEATURE: Record<number, WithingsPlanFeature> = {
|
|
1: "weight",
|
|
5: "fatFreeMass",
|
|
6: "bodyFat",
|
|
8: "fatMass",
|
|
9: "bloodPressure",
|
|
10: "bloodPressure",
|
|
11: "heartRate",
|
|
12: "bodyTemperature",
|
|
35: "spo2",
|
|
54: "spo2",
|
|
71: "bodyTemperature",
|
|
73: "skinTemperature",
|
|
76: "muscleMass",
|
|
77: "bodyWater",
|
|
88: "boneMass",
|
|
91: "pulseWaveVelocity",
|
|
123: "vo2Max",
|
|
155: "vascularAge",
|
|
170: "visceralFat",
|
|
};
|
|
|
|
const SLEEP_FIELD_FEATURE: Record<string, WithingsPlanFeature> = {
|
|
hr_average: "sleepHeartRate",
|
|
rr_average: "sleepRespiratoryRate",
|
|
sdnn_1: "sleepHrv",
|
|
spo2_average: "sleepSpo2",
|
|
sleep_score: "sleepScore",
|
|
};
|
|
|
|
/**
|
|
* The readings this connection's plan currently withholds, deduplicated and in
|
|
* display order. Empty when nothing is refused.
|
|
*/
|
|
export function withingsPlanUnavailableFeatures(
|
|
state: WithingsApiPlanState,
|
|
): WithingsPlanFeature[] {
|
|
const found = new Set<WithingsPlanFeature>();
|
|
for (const t of state.measures?.unavailable ?? []) {
|
|
const f = MEASTYPE_FEATURE[t];
|
|
if (f) found.add(f);
|
|
}
|
|
for (const field of state.sleepSummary?.unavailable ?? []) {
|
|
const f = SLEEP_FIELD_FEATURE[field];
|
|
if (f) found.add(f);
|
|
}
|
|
if ((state.ecg?.unavailable.length ?? 0) > 0) found.add("ecg");
|
|
return WITHINGS_PLAN_FEATURES.filter((f) => found.has(f));
|
|
}
|