mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(withings): ingest meastype 12 (legacy Thermo temperature) into BODY_TEMPERATURE
First-gen Withings Thermo (WBT01) reports body temperature under meastype 12. The current-gen Thermo ships meastype 71 (next commit). Both decode through the same `value * 10^unit` path the client already applies, and target the `BODY_TEMPERATURE` enum slot we added in v1.4.23 for the Apple Health passthrough. Adds `src/lib/withings/__tests__/client.test.ts` with a synthetic `getmeas` payload covering the exponent decode, unknown-meastype skip, and two-decimal rounding. Re-exports MEASURE_TYPE_MAP so the test can assert the mapping directly without importing internals.
This commit is contained in:
1 parent
cbcc0598b2
commit
ae8e2d77aa
2 files changed
+95
-2
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { MEASURE_TYPE_MAP, fetchMeasurements } from "../client";
|
||||
|
||||
/**
|
||||
* Withings client — meastype mapping + payload-parsing unit tests.
|
||||
*
|
||||
* Each new meastype gets a dedicated case that drives a synthetic
|
||||
* `measure-getmeas` payload through `fetchMeasurements()` and asserts the
|
||||
* mapped row shape. Edge cases (unknown type, missing exponent) live at
|
||||
* the end so a new mapping commit doesn't bloat the diff.
|
||||
*/
|
||||
|
||||
interface FakeMeasure {
|
||||
type: number;
|
||||
value: number;
|
||||
unit: number;
|
||||
}
|
||||
|
||||
function fakeGetmeasPayload(measures: FakeMeasure[], date = 1730000000) {
|
||||
return {
|
||||
status: 0,
|
||||
body: {
|
||||
updatetime: "2024-10-27T00:00:00Z",
|
||||
timezone: "Europe/Berlin",
|
||||
measuregrps: [
|
||||
{
|
||||
grpid: 1,
|
||||
attrib: 0,
|
||||
date,
|
||||
created: date,
|
||||
modified: date,
|
||||
measures,
|
||||
},
|
||||
],
|
||||
more: false,
|
||||
offset: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function installFetchMock(payload: unknown) {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("MEASURE_TYPE_MAP", () => {
|
||||
it("maps Withings meastype 12 (legacy Thermo) → BODY_TEMPERATURE", () => {
|
||||
expect(MEASURE_TYPE_MAP[12]).toEqual({ type: "BODY_TEMPERATURE" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchMeasurements — temperature (meastype 12)", () => {
|
||||
it("decodes the value × 10^unit exponent and emits a BODY_TEMPERATURE row", async () => {
|
||||
// 36.8 °C encoded as Withings exponent shape: value=368, unit=-1.
|
||||
installFetchMock(fakeGetmeasPayload([{ type: 12, value: 368, unit: -1 }]));
|
||||
const out = await fetchMeasurements("token");
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0]).toMatchObject({ type: "BODY_TEMPERATURE", value: 36.8 });
|
||||
});
|
||||
|
||||
it("skips an unknown meastype without throwing", async () => {
|
||||
installFetchMock(
|
||||
fakeGetmeasPayload([
|
||||
{ type: 12, value: 368, unit: -1 },
|
||||
// 9999 is not in MEASURE_TYPE_MAP → must be ignored.
|
||||
{ type: 9999, value: 1, unit: 0 },
|
||||
]),
|
||||
);
|
||||
const out = await fetchMeasurements("token");
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0].type).toBe("BODY_TEMPERATURE");
|
||||
});
|
||||
|
||||
it("rounds to two decimals (Withings sometimes ships fractional exponents)", async () => {
|
||||
// 36.825 → stored as 36.83 once `parseFloat(value.toFixed(2))` runs.
|
||||
installFetchMock(fakeGetmeasPayload([{ type: 12, value: 36825, unit: -3 }]));
|
||||
const out = await fetchMeasurements("token");
|
||||
expect(out[0].value).toBe(36.83);
|
||||
});
|
||||
});
|
||||
@@ -119,14 +119,18 @@ export async function refreshAccessToken(
|
||||
return json.body as WithingsTokenResponse;
|
||||
}
|
||||
|
||||
// Withings measure type mapping
|
||||
// Withings measure type mapping. The single source of truth is
|
||||
// `src/lib/withings/mapping.md` — keep both in sync when adding entries.
|
||||
// https://developer.withings.com/api-reference#tag/measure/operation/measure-getmeas
|
||||
const MEASURE_TYPE_MAP: Record<number, { type: string; factor?: number }> = {
|
||||
export const MEASURE_TYPE_MAP: Record<number, { type: string; factor?: number }> = {
|
||||
1: { type: "WEIGHT" }, // Weight (kg)
|
||||
9: { type: "BLOOD_PRESSURE_DIA" }, // Diastolic BP
|
||||
10: { type: "BLOOD_PRESSURE_SYS" }, // Systolic BP
|
||||
11: { type: "PULSE" }, // Heart rate
|
||||
6: { type: "BODY_FAT" }, // Body fat %
|
||||
// v1.4.25 — first-gen Thermo (WBT01) reports temperature as meastype 12;
|
||||
// current-gen ships meastype 71 instead (see below). Both canonical °C.
|
||||
12: { type: "BODY_TEMPERATURE" },
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user