Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supplemental Metrics for ECG Reading #47

Merged
merged 13 commits into from
Feb 19, 2024
107 changes: 107 additions & 0 deletions PAWS/ECGRecordings/ECGRecording.swift
MatthewTurk247 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,110 @@
}
}
}

extension HKElectrocardiogram {
private var oneDayPredicate: NSPredicate {
HKQuery.predicateForSamples(
withStart: Calendar.current.date(byAdding: .day, value: -1, to: self.startDate), // 24 hours before recording.
end: self.startDate,
options: .strictStartDate
)
}

Check warning on line 51 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L45-L51

Added lines #L45 - L51 were not covered by tests

private var fiveMinutePredicate: NSPredicate {
HKQuery.predicateForSamples(
withStart: Calendar.current.date(byAdding: .minute, value: -5, to: self.startDate), // 5 minutes before recording.
end: self.startDate,
options: .strictStartDate
)
}

Check warning on line 59 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L53-L59

Added lines #L53 - L59 were not covered by tests

var precedingPulseRates: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.heartRate))
}

Check warning on line 64 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L62-L64

Added lines #L62 - L64 were not covered by tests
}

var precedingVo2Max: HKQuantitySample? {
get async throws {
try await precedingSamples(
forType: HKQuantityType(.vo2Max),
sortDescriptors: [SortDescriptor(\.startDate, order: .reverse)],
limit: 1
)
.first
}

Check warning on line 75 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L68-L75

Added lines #L68 - L75 were not covered by tests
}

var precedingPhysicalEffort: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.physicalEffort))
}

Check warning on line 81 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L79-L81

Added lines #L79 - L81 were not covered by tests
}

var precedingStepCount: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.stepCount))
}

Check warning on line 87 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L85-L87

Added lines #L85 - L87 were not covered by tests
}

var precedingActiveEnergy: [HKQuantitySample] {
get async throws {
try await precedingSamples(forType: HKQuantityType(.activeEnergyBurned))
}

Check warning on line 93 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L91-L93

Added lines #L91 - L93 were not covered by tests
}

private func precedingSamples(
forType type: HKSampleType,
sortDescriptors: [SortDescriptor<HKSample>] = [SortDescriptor(\.startDate)],
limit: Int? = nil
) async throws -> [HKSample] {
let store = HKHealthStore()
let queryDescriptor = HKSampleQueryDescriptor(
predicates: [.sample(type: type, predicate: self.fiveMinutePredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

// If something is available in last 5 minutes since recording, return those samples.
if let result = try? await queryDescriptor.result(for: store), !result.isEmpty {
return result
}

// Otherwise, request the last 24 hours of samples.
let extendedQueryDescriptor = HKSampleQueryDescriptor(
predicates: [.sample(type: type, predicate: self.oneDayPredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

return try await extendedQueryDescriptor.result(for: store)
}

Check warning on line 121 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L100-L121

Added lines #L100 - L121 were not covered by tests

private func precedingSamples(
forType type: HKQuantityType,
sortDescriptors: [SortDescriptor<HKQuantitySample>] = [SortDescriptor(\.startDate)],
limit: Int? = nil
) async throws -> [HKQuantitySample] {
let store = HKHealthStore()
let queryDescriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: type, predicate: self.fiveMinutePredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

// If something is available in last 5 minutes since recording, return those samples.
if let result = try? await queryDescriptor.result(for: store), !result.isEmpty {
return result
}

// Otherwise, request the last 24 hours of samples.
let extendedQueryDescriptor = HKSampleQueryDescriptor(
predicates: [.quantitySample(type: type, predicate: self.oneDayPredicate)],
sortDescriptors: sortDescriptors,
limit: limit
)

return try await extendedQueryDescriptor.result(for: store)
}

Check warning on line 148 in PAWS/ECGRecordings/ECGRecording.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/ECGRecordings/ECGRecording.swift#L127-L148

Added lines #L127 - L148 were not covered by tests
}
29 changes: 27 additions & 2 deletions PAWS/PAWSDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class PAWSDelegate: SpeziAppDelegate {
// Collection starts at the time the user consents and lasts for 1 month.
let sharedPredicate = HKQuery.predicateForSamples(
withStart: healthKitStartDate,
end: Calendar.current.date(byAdding: DateComponents(month: 1), to: healthKitStartDate),
end: Calendar.current.date(byAdding: DateComponents(month: 6), to: healthKitStartDate),
MatthewTurk247 marked this conversation as resolved.
Show resolved Hide resolved
options: .strictEndDate
)

Expand All @@ -90,7 +90,32 @@ class PAWSDelegate: SpeziAppDelegate {
CollectSamples(
Set(HKElectrocardiogram.correlatedSymptomTypes),
predicate: sharedPredicate,
deliverySetting: .background(saveAnchor: false)
deliverySetting: .background(saveAnchor: true)
)
CollectSample(
HKQuantityType(.heartRate),
predicate: sharedPredicate,
deliverySetting: .background(saveAnchor: true)
)
CollectSample(
HKQuantityType(.vo2Max),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
CollectSample(
HKQuantityType(.physicalEffort),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
CollectSample(
HKQuantityType(.stepCount),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
CollectSample(
HKQuantityType(.activeEnergyBurned),
predicate: sharedPredicate,
deliverySetting: .manual(safeAnchor: false)
)
}
}
Expand Down
23 changes: 23 additions & 0 deletions PAWS/PAWSStandard.swift
MatthewTurk247 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,43 @@


func add(sample: HKSample) async {
var supplementalMetrics: [HKSample] = []

Check warning on line 72 in PAWS/PAWSStandard.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/PAWSStandard.swift#L71-L72

Added lines #L71 - L72 were not covered by tests
if let hkElectrocardiogram = sample as? HKElectrocardiogram {
ecgStorage.hkElectrocardiograms.append(hkElectrocardiogram)

do {
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingPulseRates)
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingPhysicalEffort)
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingStepCount)
supplementalMetrics.append(contentsOf: try await hkElectrocardiogram.precedingActiveEnergy)

if let precedingVo2Max = try await hkElectrocardiogram.precedingVo2Max {
supplementalMetrics.append(precedingVo2Max)
}
} catch {
logger.log("Could not access HealthKit sample: \(error)")
}

Check warning on line 87 in PAWS/PAWSStandard.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/PAWSStandard.swift#L75-L87

Added lines #L75 - L87 were not covered by tests
}

if let mockWebService {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
let jsonRepresentation = (try? String(data: encoder.encode(sample.resource), encoding: .utf8)) ?? ""
try? await mockWebService.upload(path: "healthkit/\(sample.uuid.uuidString)", body: jsonRepresentation)

for metric in supplementalMetrics {
try? await mockWebService.upload(path: "healthkit/\(metric.uuid.uuidString)", body: (try? String(data: encoder.encode(metric.resource), encoding: .utf8)) ?? "")
}

Check warning on line 99 in PAWS/PAWSStandard.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/PAWSStandard.swift#L95-L99

Added lines #L95 - L99 were not covered by tests
return
}

do {
try await healthKitDocument(id: sample.id).setData(from: sample.resource)
for metric in supplementalMetrics {
try await healthKitDocument(id: sample.id).setData(from: metric.resource)
}

Check warning on line 107 in PAWS/PAWSStandard.swift

View check run for this annotation

Codecov / codecov/patch

PAWS/PAWSStandard.swift#L105-L107

Added lines #L105 - L107 were not covered by tests
} catch {
logger.error("Could not store HealthKit sample: \(error)")
}
Expand Down
Loading