// Health Sync — reads Apple Health via Scripting app and POSTs to health-mcp server // Run via Scripting app automation at 9:30am daily // Server: http://chronos:9432/ingest // // Uses Scripting app's Health API: // Health.queryQuantitySamples(type, options) — returns samples // sample.quantityValue(unit) — extracts numeric value // HealthUnit / HealthMetricPrefix — unit construction // // Docs: https://scriptingapp.github.io/guide/Device%20Capabilities/Health/ // Variables used by Scripting app. These must be at the very top of the file. // icon-color: deep-blue; icon-glyph: heart; const SERVER_URL = "http://chronos:9432/ingest"; const API_KEY = "333f09ce904e9cbbe3b578fda393be92869122c7222dbccd"; // --- Units for each quantity type --- // HealthUnit API: https://scriptingapp.github.io/guide/Device%20Capabilities/Health/HealthUnit const U = { kg: HealthUnit.gramUnit(HealthMetricPrefix.kilo), mmHg: HealthUnit.fromString("mmHg"), bpm: HealthUnit.count().divided(HealthUnit.minute()), count: HealthUnit.count(), percent: HealthUnit.percent() }; // Read the most recent sample of a type. Returns { value, startDate } or null. async function readLatest(type, unit) { try { const samples = await Health.queryQuantitySamples(type, { limit: 1, sortDescriptors: [{ key: "startDate", order: "reverse" }] }); if (!samples || samples.length === 0) { console.log(`readLatest(${type}): no samples`); return null; } const s = samples[0]; const value = s.quantityValue(unit); console.log(`readLatest(${type}): value=${value} date=${s.startDate}`); return { value, startDate: s.startDate }; } catch (e) { console.log(`readLatest(${type}): error ${e}`); return null; } } // Sum all samples of a type since midnight. Returns a number or null. async function readTodaySum(type, unit) { try { const start = new Date(); start.setHours(0, 0, 0, 0); const samples = await Health.queryQuantitySamples(type, { startDate: start, endDate: new Date() }); if (!samples || samples.length === 0) { console.log(`readTodaySum(${type}): no samples`); return null; } let total = 0; for (const s of samples) { total += s.quantityValue(unit); } console.log(`readTodaySum(${type}): total=${total}`); return total; } catch (e) { console.log(`readTodaySum(${type}): error ${e}`); return null; } } function todayDate() { return new Date().toISOString().slice(0, 10); } async function notify(body) { console.log(`notify: ${body}`); const n = new Notification(); n.title = "Health Sync"; n.body = body; await n.schedule(); } // --- Read all health metrics --- console.log("=== Health Sync starting ==="); const weight = await readLatest("bodyMass", U.kg); const bpSys = await readLatest("bloodPressureSystolic", U.mmHg); const bpDia = await readLatest("bloodPressureDiastolic", U.mmHg); const hr = await readLatest("heartRate", U.bpm); const restingHr = await readLatest("restingHeartRate", U.bpm); const steps = await readTodaySum("stepCount", U.count); const spo2 = await readLatest("oxygenSaturation", U.percent); console.log(`Results: weight=${weight?.value} bpSys=${bpSys?.value} bpDia=${bpDia?.value} hr=${hr?.value} rhr=${restingHr?.value} steps=${steps} spo2=${spo2?.value}`); // --- Build samples array — only include metrics that have data --- const samples = []; if (weight) { samples.push({ type: "weight", timestamp_utc: weight.startDate.toISOString(), value: weight.value, source: "apple_health" }); } if (bpSys && bpDia) { const bpValue = { systolic: bpSys.value, diastolic: bpDia.value }; if (hr) bpValue.pulse = hr.value; samples.push({ type: "blood_pressure", timestamp_utc: bpSys.startDate.toISOString(), value: bpValue, source: "omron" }); } if (hr && !bpSys) { samples.push({ type: "heart_rate", timestamp_utc: hr.startDate.toISOString(), value: hr.value, source: "apple_health" }); } if (restingHr) { samples.push({ type: "resting_heart_rate", timestamp_utc: restingHr.startDate.toISOString(), value: restingHr.value, source: "apple_health" }); } if (steps !== null) { samples.push({ type: "steps", timestamp_utc: todayDate(), value: steps, source: "apple_health" }); } if (spo2) { samples.push({ type: "spo2", timestamp_utc: spo2.startDate.toISOString(), value: spo2.value * 100, // HealthKit stores 0-1, server expects % source: "apple_health" }); } console.log(`Total samples to send: ${samples.length}`); // --- POST to server --- if (samples.length === 0) { await notify("No health data found today"); } else { const req = new Request(SERVER_URL); req.method = "POST"; req.headers = { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" }; req.body = JSON.stringify({ samples: samples }); try { const res = await req.loadJSON(); console.log(`Server response: ${JSON.stringify(res)}`); await notify(`Sent ${res.ingested || samples.length} samples`); } catch (e) { console.log(`POST error: ${e}`); await notify(`Error: ${e.message || e}`); } } // end else console.log("=== Health Sync done ===");