Example: HealthKit Export


#1

For ages I’ve always wanted to export HealthKit data so that I can visualise it. I was incredibly happy to find that Scriptable has a Health API that lets you retrieve all your data points. This script loops over several HealthKit measurement types and exports them to either json, csv, or sql and writes them to your iCloud storage.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: beaker;
const config = {
  sampleLimit: 100000, // The maximum number of samples pulled back from each category
  startDate: new Date("2018-01-01"), // The starting date to provide samples from
  outputFormat: "json", // One of "json", "csv", "sql"
  outputFileName: "health", // Filename to use for export
  debug: false, // Output debug data
  measurements: {
    height: 'cm',
    bodyMass: 'kg',
    bodyMassIndex: 'count',
    leanBodyMass: 'kg',
    bodyFatPercentage: '%',
    heartRate: 'count/min',
    bodyTemperature: 'degC',
    bloodPressureSystolic: 'cmAq',
    bloodPressureDiastolic: 'cmAq',
    bloodGlucose: 'mmol/L',
    insulinDelivery: 'mg',
    respiratoryRate: 'count/min',
    stepCount: 'count',
    distanceWalkingRunning: 'm',
    distanceCycling: 'm',
    pushCount: 'count',
    distanceWheelchair: 'm',
    swimmingStrokeCount: 'count',
    distanceSwimming: 'm',
    basalEnergyBurned: 'cal',
    activeEnergyBurned: 'cal',
    flightsClimbed: 'count',
    nikeFuel: 'count',
    appleExerciseTime: 'min',
    basalBodyTemperature: 'degC'
  }
}

const health = new Health()

function leftPad(string, character, length) {
  let output = `${string}`
  while (output.length < length) {
    output = `${character}${output}`
  }
  
  return output
}

//YYYY-MM-DD H:i:s
function formatSqlDate(date) {
  let year = date.getFullYear()
  let month = leftPad(date.getMonth() + 1, '0', 2)
  let day = leftPad(date.getDate(), '0', 2)
  
  let hour = leftPad(date.getHours(), '0', 2)
  let minute = leftPad(date.getMinutes(), '0', 2)
  let second = leftPad(date.getSeconds(), '0', 2)
  
  return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}

const debugLog = (message) => {
  if (config.debug) {
    console.log(`[DEBUG] ${message}`)
  }
}

const output = {}
for (const measurement in config.measurements) {
  const unit = config.measurements[measurement]
  if (unit === '' || unit === null) {
    continue
  }
  
  console.log(`Gathering ${measurement} in ${unit}`)
  health.setTypeIdentifier(measurement)
  health.setUnit(unit)
  health.setLimit(config.sampleLimit)
  health.setDescendingSorting()
  
  let samples = await health.quantitySamples()
  for (const sample of samples) {
    sample.startDate = new Date(sample.startDate)
    sample.endDate = new Date(sample.endDate)
  }
  
  debugLog(`Found ${samples.length} samples`)
  if (samples.length > 0) {
    debugLog(`First sample starts at ${samples[0].startDate}`)
    debugLog(`Last sample starts at ${samples[samples.length-1].startDate}`)
  }
  
  if (config.startDate !== null) {
    samples = samples.filter(sample => sample.startDate >= config.startDate)
  }
  
  output[measurement] = {
    unit: unit,
    samples: samples
  }
}

// Format and write file
console.log(`Writing to ${config.outputFileName}.${config.outputFormat}`)

const dataFormatters = {};

dataFormatters.json = function (data) {  
  return JSON.stringify(data)
}

dataFormatters.csv = function (data) {
  let output = "measurement,unit,startDate,endDate,value\n"
  
  for (const measurementType in data) {
    const unit = data[measurementType]['unit']
    
    for (const measurementData of data[measurementType]['samples']) {
      output += `${measurementType},${unit},${formatSqlDate(measurementData.startDate)},${formatSqlDate(measurementData.endDate)},${measurementData.value}\n`
    }
  }
  
  return output
}

dataFormatters.sql = function (data) {
  let output =
`CREATE TABLE IF NOT EXISTS measurements (
  measurementType VARCHAR(32),
  unit VARCHAR(32),
  startDate DATETIME,
  endDate DATETIME,
  value FLOAT
);

INSERT INTO measurements (measurementType, unit, startDate, endDate, value) VALUES \n`
  
  let hasSamples = false
  for (const measurementType in data) {
    const unit = data[measurementType]['unit']
    
    for (const measurementData of data[measurementType]['samples']) {
      hasSamples = true
      output += `('${measurementType}','${unit}','${formatSqlDate(measurementData.startDate)}','${formatSqlDate(measurementData.endDate)}',${measurementData.value}),\n`
    }
  }
  
  if (hasSamples) {
    output = output.substring(0, output.length - 2) + ";\n"
  } else {
    output = "; No samples available"
  }
  
  return output

}

const fileManager = FileManager.iCloud()
const outputPath = fileManager.joinPath(fileManager.documentsDirectory(), `${config.outputFileName}.${config.outputFormat}`)
fileManager.writeString(outputPath, dataFormatters[config.outputFormat](output))

#2

That is super cool! :star_struck: Thanks for sharing.


#3

Was running Scriptable on my iPad only, but now that iOS 12 GM is out I could finally try this on my iPhone. The export in the Health App does not work for me (it results in a corrupt small zip file), so I’m super happy to try this. Works great! :smile:

For a moment I was surprised by the weird blood pressure values, but after changing the unit to mmHg they looked familiar again… Is this another measurement (unit) where the US and Europe use a different system?

PS: Too bad that Apple rejected the use of HealthKit in the App Store version:


#4

Yes, it’s really too bad that Apple rejected the app for using HealthKit. Unfortunately Scriptable will launch without the Health API.
I still have hope that Scriptable can integrate with HealthKit in a future update. I’m very excited about the API and would like to have it back in the app again.


#5

As Workflow had/has it there does seem to be a precedent!


#6

Did Workflow have HealthKit access before Apple them bought?


#7

Yes! They did. That’s the curious thing, some apps seem to be able to use the data and others are rejected for essentially the same inclusion.


#8

And that ladies and gentlemen is the unfortunate ongoing saga of the app store. :pensive:

But at least these things can be contested and with prior approvals to cite it does at least improve the chances of subsequent approval.


#9

It’s a real shame that Apple decided to reject its use of the HealthKit APIs. Fingers crossed they’ll allow it in a future update!

@rob I just grabbed the first measurement unit out of the HealthKit docs. I imagine mmHg makes more sense!


#10

Well, after seeing values I did not expect I had to open the App for my blood pressure meter to find out which unit it uses…


#11

It’s been a while since I’ve attempted to publish anything to the app store, but from what I remember it’s mostly an automated routine until you dispute it. I think the packaged app is getting rejected by a bot. Again, I forget, but is there still an option to “dispute” or call them?

I remember one app I published for a client was rejected because it had the word “mac” in it; the app was named Schumacher. lol!

I hope you can get this approved eventually. Nice work at any rate!


#12

Any update on this?

I’m having major problems with the App that syncs data from my scale and blood pressure meter and it looks like I have to uninstall that App in an attempt to get it working again. I would like to put back its data in HealthKit in bulk (if the App removes them during sync, which seems to be the case). This can possibly be done in Shortcuts, but it’s most likely much easier in Scriptable…


#13

It’s a work in progress but unfortunately it won’t make the cut for version 1.3.