From 4158a02966cdc97f01534474f1689d1a529b29c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 13:24:01 +0200 Subject: [PATCH] feat(withings): ingest meastype 35 (legacy SpO2) into OXYGEN_SATURATION Older Withings firmware and some partner SDK examples report SpO2 under meastype 35 instead of 54. Mapping both into OXYGEN_SATURATION prevents a quiet drop on legacy devices and is compatible with mixed fleets (the existing meastype 54 path stays in place). --- src/lib/withings/__tests__/client.test.ts | 29 +++++++++++++++++++++++ src/lib/withings/client.ts | 4 ++++ 2 files changed, 33 insertions(+) diff --git a/src/lib/withings/__tests__/client.test.ts b/src/lib/withings/__tests__/client.test.ts index 906621084..e61b8ca49 100644 --- a/src/lib/withings/__tests__/client.test.ts +++ b/src/lib/withings/__tests__/client.test.ts @@ -60,6 +60,35 @@ describe("MEASURE_TYPE_MAP", () => { it("maps Withings meastype 71 (current-gen Thermo) → BODY_TEMPERATURE", () => { expect(MEASURE_TYPE_MAP[71]).toEqual({ type: "BODY_TEMPERATURE" }); }); + + it("maps Withings meastype 35 (legacy SpO2) → OXYGEN_SATURATION", () => { + expect(MEASURE_TYPE_MAP[35]).toEqual({ type: "OXYGEN_SATURATION" }); + }); +}); + +describe("fetchMeasurements — SpO2 alt code (meastype 35)", () => { + it("decodes a legacy-firmware SpO2 reading into OXYGEN_SATURATION %", async () => { + // 97% as Withings exponent encoding: value=97, unit=0. + installFetchMock(fakeGetmeasPayload([{ type: 35, value: 97, unit: 0 }])); + const out = await fetchMeasurements("token"); + expect(out).toHaveLength(1); + expect(out[0]).toMatchObject({ type: "OXYGEN_SATURATION", value: 97 }); + }); + + it("co-exists with meastype 54 in the same payload (mixed firmware)", async () => { + installFetchMock( + fakeGetmeasPayload([ + { type: 35, value: 96, unit: 0 }, + { type: 54, value: 98, unit: 0 }, + ]), + ); + const out = await fetchMeasurements("token"); + expect(out).toHaveLength(2); + expect(out.map((r) => r.type)).toEqual([ + "OXYGEN_SATURATION", + "OXYGEN_SATURATION", + ]); + }); }); describe("fetchMeasurements — body temperature (meastype 71)", () => { diff --git a/src/lib/withings/client.ts b/src/lib/withings/client.ts index 5cbbfa3e8..7c1b67ce6 100644 --- a/src/lib/withings/client.ts +++ b/src/lib/withings/client.ts @@ -138,6 +138,10 @@ export const MEASURE_TYPE_MAP: Record 77: { type: "TOTAL_BODY_WATER" }, // Hydration / water mass (kg) 88: { type: "BONE_MASS" }, // Bone mass (kg) 54: { type: "OXYGEN_SATURATION" }, // SpO2 (% — only ScanWatch / pulse-ox products) + // v1.4.25 — older Withings firmware (and some SDK examples) report + // SpO2 under meastype 35 instead of 54. Both target OXYGEN_SATURATION + // in percent; the exponent decode handles 0.97 vs 97 transparently. + 35: { type: "OXYGEN_SATURATION" }, }; export interface WithingsMeasure {