// @ts-nocheck // Health Sync v7 — 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 APIs: // Health.queryQuantitySamples(type, options) — global API // sample.quantityValue(unit) — extracts numeric value // HealthUnit / HealthUnitPrefix — unit construction (global) // fetch() — standard Fetch API (with allowInsecureRequest for HTTP) // Notification — imported from "scripting" // Script — imported from "scripting" for Script.exit() // // Docs: https://scriptingapp.github.io/guide/Device%20Capabilities/Health/ import { Script, Notification } from "scripting" // icon-color: deep-blue; icon-glyph: heart; async function main() { const SERVER_URL = "http://chronos:9432/ingest" const API_KEY = "333f09ce904e9cbbe3b578fda393be92869122c7222dbccd" // --- Units for each quantity type --- const U = { kg: HealthUnit.gramUnit(HealthUnitPrefix.kilo), mmHg: HealthUnit.millimeterOfMercury(), 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. const readLatest = async (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. const readTodaySum = async (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 } } const todayDate = () => { return new Date().toISOString().slice(0, 10) } const notify = async (body) => { console.log(`notify: ${body}`) await Notification.schedule({ title: "Health Sync", body: body }) } // --- 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 { try { const response = await fetch(SERVER_URL, { method: "POST", headers: { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ samples }), allowInsecureRequest: true }) const res = await response.json() 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}`) } } console.log("=== Health Sync done ===") } main().then(() => Script.exit())