nutrition-mcp/scripts/health-sync.js

167 lines
No EOL
4.5 KiB
JavaScript

// Health Sync — reads Apple Health and POSTs to nutrition-mcp server
// Run via Scriptable automation at 9:30am daily
// Server: http://chronos:9432/ingest
//
// Uses Scriptable's Health API: new Health() instance with string units.
const SERVER_URL = "http://chronos:9432/ingest";
const API_KEY = "333f09ce904e9cbbe3b578fda393be92869122c7222dbccd";
// Read the most recent sample of a type. Returns { value, startDate } or null.
async function readLatest(type, unit) {
try {
const h = new Health();
h.setTypeIdentifier(type);
h.setUnit(unit);
h.setLimit(1);
h.setDescendingSorting();
const samples = await h.quantitySamples();
if (!samples || samples.length === 0) {
console.log(`readLatest(${type}): no samples`);
return null;
}
const s = samples[0];
console.log(`readLatest(${type}): value=${s.value} date=${s.startDate}`);
return { value: s.value, startDate: new Date(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 h = new Health();
h.setTypeIdentifier(type);
h.setUnit(unit);
const samples = await h.quantitySamples();
if (!samples || samples.length === 0) {
console.log(`readTodaySum(${type}): no samples`);
return null;
}
let total = 0;
for (const s of samples) {
const sd = new Date(s.startDate);
if (sd >= start) total += s.value;
}
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", "kg");
const bpSys = await readLatest("bloodPressureSystolic", "mmHg");
const bpDia = await readLatest("bloodPressureDiastolic", "mmHg");
const hr = await readLatest("heartRate", "count/min");
const restingHr = await readLatest("restingHeartRate", "count/min");
const steps = await readTodaySum("stepCount", "count");
const spo2 = await readLatest("oxygenSaturation", "%");
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 ===");