mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(withings): expand webhook subscriptions to activity + sleep v2
WITHINGS_NOTIFY_APPLIS grows from [1, 2, 4] to [1, 2, 4, 16, 44]: appli=16 (Activity) and appli=44 (Sleep v2) join the webhook-primary set so the new sync routines fire in seconds rather than waiting for the hourly cron fallback. setupWebhook iterates the new list unchanged — Withings has no batch subscribe endpoint, so every appli is its own POST. The two new categories require the user.activity OAuth scope from W5d; legacy connections that never reconnected return 503/293 on those subscribes, which setupWebhook already logs as a per-appli warning while keeping the remaining subscriptions live.
This commit is contained in:
1 parent
84af00d3a9
commit
e504faa7b6
2 files changed
+73
-7
No files matched your search
@@ -5,6 +5,7 @@ import {
|
||||
fetchMeasurements,
|
||||
hasActivityScope,
|
||||
parseWithingsScope,
|
||||
subscribeWebhook,
|
||||
} from "../client";
|
||||
import { WITHINGS_NOTIFY_APPLIS } from "../sync";
|
||||
|
||||
@@ -138,8 +139,11 @@ describe("WITHINGS_OAUTH_SCOPE + scope helpers", () => {
|
||||
});
|
||||
|
||||
describe("WITHINGS_NOTIFY_APPLIS", () => {
|
||||
it("subscribes to weight + temperature + pressure categories", () => {
|
||||
expect(WITHINGS_NOTIFY_APPLIS).toEqual([1, 2, 4]);
|
||||
it("subscribes to weight + temperature + pressure + activity + sleep categories", () => {
|
||||
// v1.4.25 W17b/c — activity (16) + sleep v2 (44) join the
|
||||
// webhook-primary set so the new sync routines fire in seconds
|
||||
// rather than waiting for the hourly cron fallback.
|
||||
expect(WITHINGS_NOTIFY_APPLIS).toEqual([1, 2, 4, 16, 44]);
|
||||
});
|
||||
|
||||
it("contains every appli for the meastypes we ingest", () => {
|
||||
@@ -150,12 +154,68 @@ describe("WITHINGS_NOTIFY_APPLIS", () => {
|
||||
// - 9, 10, 11, 35, 54 → appli=4 (BP + pulse + SpO2)
|
||||
// - 123 → appli=1 (VO2 max is part of the weight category in
|
||||
// Withings' bucketing; verified against the developer guide).
|
||||
// - steps + distance + active energy + floors → appli=16 (Activity)
|
||||
// - sleep stage segments → appli=44 (Sleep v2)
|
||||
const ingested = Object.keys(MEASURE_TYPE_MAP).map(Number).sort();
|
||||
expect(ingested).toContain(12);
|
||||
expect(ingested).toContain(71);
|
||||
expect(ingested).toContain(35);
|
||||
expect(ingested).toContain(123);
|
||||
});
|
||||
|
||||
it("includes 16 (activity) and 44 (sleep v2) — the W17b/c webhook additions", () => {
|
||||
// Explicit guard so a future refactor that re-sorts the array
|
||||
// doesn't silently drop the activity / sleep webhook trigger.
|
||||
expect(WITHINGS_NOTIFY_APPLIS).toContain(16);
|
||||
expect(WITHINGS_NOTIFY_APPLIS).toContain(44);
|
||||
});
|
||||
});
|
||||
|
||||
describe("subscribeWebhook — appli payload", () => {
|
||||
it("POSTs `appli=16` to Withings notify when called with the activity category", async () => {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
status: 200,
|
||||
json: async () => ({ status: 0 }),
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await subscribeWebhook(
|
||||
"token",
|
||||
"https://healthlog.example.com/api/withings/webhook/secret",
|
||||
16,
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [, init] = fetchMock.mock.calls[0] as unknown as [
|
||||
string,
|
||||
RequestInit,
|
||||
];
|
||||
const body = String(init.body);
|
||||
expect(body).toContain("action=subscribe");
|
||||
expect(body).toContain("appli=16");
|
||||
});
|
||||
|
||||
it("POSTs `appli=44` to Withings notify when called with the sleep v2 category", async () => {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
status: 200,
|
||||
json: async () => ({ status: 0 }),
|
||||
}));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await subscribeWebhook(
|
||||
"token",
|
||||
"https://healthlog.example.com/api/withings/webhook/secret",
|
||||
44,
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [, init] = fetchMock.mock.calls[0] as unknown as [
|
||||
string,
|
||||
RequestInit,
|
||||
];
|
||||
const body = String(init.body);
|
||||
expect(body).toContain("appli=44");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchMeasurements — VO2 max (meastype 123)", () => {
|
||||
|
||||
@@ -294,15 +294,21 @@ export function extractWithingsStatus(message: string): string | undefined {
|
||||
* - 1 — weight + body composition (meastypes 1, 5, 6, 8, 88)
|
||||
* - 2 — temperature (meastypes 12, 71, 73)
|
||||
* - 4 — pressure family (BP dia 9, BP sys 10, pulse 11, SpO2 54)
|
||||
* - 16 — activity (steps, distance, active energy, floors climbed);
|
||||
* v1.4.25 W17b webhook-primary trigger for the new
|
||||
* `syncUserActivity` routine.
|
||||
* - 44 — sleep v2 (per-stage segments + nightly summary); v1.4.25 W17c
|
||||
* webhook-primary trigger for the new `syncUserSleep` routine.
|
||||
*
|
||||
* Without 2 and 4, BP and temperature readings flow only through the
|
||||
* hourly poll fallback. Adding them removes up to an hour of latency
|
||||
* on a freshly-taken BP reading without changing the OAuth scope.
|
||||
*
|
||||
* Sleep (appli=44) and activity (appli=16) ship alongside the
|
||||
* corresponding sync routines in v1.4.26.
|
||||
* on a freshly-taken BP reading without changing the OAuth scope. The
|
||||
* activity + sleep categories require the `user.activity` scope from
|
||||
* W5d; legacy connections that never reconnected sit on `user.metrics`
|
||||
* only and the subscribe call returns 503/293 — `setupWebhook` logs
|
||||
* the failure and keeps the remaining appli subscriptions.
|
||||
*/
|
||||
export const WITHINGS_NOTIFY_APPLIS = [1, 2, 4] as const;
|
||||
export const WITHINGS_NOTIFY_APPLIS = [1, 2, 4, 16, 44] as const;
|
||||
|
||||
/**
|
||||
* Subscribe to every Withings notify category HealthLog ingests. Each
|
||||
|
||||
Reference in new issue
Block a user