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

VIT-7905: Introduce VitalResource electrocardiogram, afib_burden and heart_rate_alert cases #255

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public enum TimeSeriesData: Equatable, Encodable {
case steps([LocalQuantitySample])
case vo2Max([LocalQuantitySample])
case temperature([LocalQuantitySample])
case afibBurden([LocalQuantitySample])

public var payload: Encodable {
switch self {
Expand All @@ -91,7 +92,7 @@ public enum TimeSeriesData: Equatable, Encodable {
let .nutrition(.water(samples)), let .mindfulSession(samples),
let .caloriesActive(samples), let .caloriesBasal(samples), let .distance(samples),
let .floorsClimbed(samples), let .steps(samples), let .vo2Max(samples),
let .respiratoryRate(samples), let .temperature(samples):
let .respiratoryRate(samples), let .temperature(samples), let .afibBurden(samples):
return samples

case let .bloodPressure(samples):
Expand All @@ -107,7 +108,7 @@ public enum TimeSeriesData: Equatable, Encodable {
let .nutrition(.water(samples)), let .mindfulSession(samples),
let .caloriesActive(samples), let .caloriesBasal(samples), let .distance(samples),
let .floorsClimbed(samples), let .steps(samples), let .vo2Max(samples),
let .respiratoryRate(samples), let .temperature(samples):
let .respiratoryRate(samples), let .temperature(samples), let .afibBurden(samples):
return samples.count

case let .bloodPressure(samples):
Expand Down Expand Up @@ -149,6 +150,8 @@ public enum TimeSeriesData: Equatable, Encodable {
return "respiratory_rate"
case .temperature:
return "temperature"
case .afibBurden:
return "afib_burden"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ public enum VitalResource: Equatable, Hashable, Codable {
@_spi(VitalSDKInternals)
public var priority: Int {
switch self {
case .activity, .body, .workout, .menstrualCycle, .profile:
return 0
case .sleep, .individual(.vo2Max), .vitals(.bloodOxygen), .vitals(.bloodPressure),
.vitals(.glucose), .vitals(.heartRateVariability),
.nutrition(.water), .nutrition(.caffeine),
.vitals(.mindfulSession), .vitals(.temperature), .vitals(.respiratoryRate), .meal:
case .activity, .body, .profile:
return 1
case .sleep, .menstrualCycle, .meal:
return 4
case .workout, .individual(.vo2Max), .nutrition(.water), .nutrition(.caffeine):
return 8
case .electrocardiogram, .heartRateAlert, .afibBurden:
return 12
case .vitals(.bloodOxygen), .vitals(.bloodPressure),
.vitals(.glucose), .vitals(.heartRateVariability),
.vitals(.mindfulSession), .vitals(.temperature), .vitals(.respiratoryRate):
return 16
case .individual(.distanceWalkingRunning), .individual(.steps), .individual(.floorsClimbed):
return 2
return 32
case .vitals(.heartRate), .individual(.activeEnergyBurned), .individual(.basalEnergyBurned):
return 3
return 64
case .individual(.exerciseTime), .individual(.weight), .individual(.bodyFat):
return Int.max
}
Expand Down Expand Up @@ -76,6 +81,12 @@ public enum VitalResource: Equatable, Hashable, Codable {
return .respiratoryRate
case .meal:
return BackfillType.meal
case .afibBurden:
return .afibBurden
case .electrocardiogram:
return .electrocardiogram
case .heartRateAlert:
return .heartRateAlert
}
}

Expand Down Expand Up @@ -171,7 +182,10 @@ public enum VitalResource: Equatable, Hashable, Codable {
case individual(Individual)
case nutrition(Nutrition)
case meal

case electrocardiogram
case heartRateAlert
case afibBurden

public static var all: [VitalResource] = [
.profile,
.body,
Expand Down Expand Up @@ -201,6 +215,10 @@ public enum VitalResource: Equatable, Hashable, Codable {

.nutrition(.water),
.nutrition(.caffeine),

.electrocardiogram,
.heartRateAlert,
.afibBurden,
]

public var logDescription: String {
Expand All @@ -225,6 +243,12 @@ public enum VitalResource: Equatable, Hashable, Codable {
return individual.logDescription
case let .nutrition(nutrition):
return nutrition.logDescription
case .electrocardiogram:
return "electrocardiogram"
case .heartRateAlert:
return "heartRateAlert"
case .afibBurden:
return "afibBurden"
}
}
}
Expand Down Expand Up @@ -265,4 +289,6 @@ public struct BackfillType: RawRepresentable, Codable, Equatable, Hashable {
public static let electrocardiogram = BackfillType(rawValue: "electrocardiogram")
public static let temperature = BackfillType(rawValue: "temperature")
public static let menstrualCycle = BackfillType(rawValue: "menstrual_cycle")
public static let heartRateAlert = BackfillType(rawValue: "heart_rate_alert")
public static let afibBurden = BackfillType(rawValue: "afib_burden")
}
15 changes: 15 additions & 0 deletions Sources/VitalHealthKit/HealthKit/Abstractions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ extension VitalHealthKitStore {
HKQuantityType.quantityType(forIdentifier: .dietarySelenium)!:
return [.meal]

case
HKCategoryType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!,
HKCategoryType.categoryType(forIdentifier: .lowHeartRateEvent)!,
HKCategoryType.categoryType(forIdentifier: .highHeartRateEvent)!:
return [.heartRateAlert]

case HKElectrocardiogramType.electrocardiogramType():
return [.electrocardiogram]

default:
if #available(iOS 15.0, *) {
switch type {
Expand All @@ -208,6 +217,12 @@ extension VitalHealthKitStore {
HKCategoryType.categoryType(forIdentifier: .irregularMenstrualCycles)!,
HKCategoryType.categoryType(forIdentifier: .infrequentMenstrualCycles)!:
return [.menstrualCycle]


case
HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)!:
return [.afibBurden]

default:
break
}
Expand Down
24 changes: 24 additions & 0 deletions Sources/VitalHealthKit/HealthKit/HealthKitReads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,30 @@ func read(

return (.timeSeries(.respiratoryRate(payload.samples)), payload.anchors)


case .electrocardiogram:
// VIT-7905: To be implemented
return (nil, [])

case .heartRateAlert:
// VIT-7905: To be implemented
return (nil, [])

case .afibBurden:
if #available(iOS 16.0, *) {
let payload = try await handleTimeSeries(
.atrialFibrillationBurden,
healthKitStore: healthKitStore,
vitalStorage: vitalStorage,
startDate: instruction.query.lowerBound,
endDate: instruction.query.upperBound
)

return (.timeSeries(.afibBurden(payload.samples)), payload.anchors)
} else {
return (nil, [])
}

case .individual(.exerciseTime), .individual(.bodyFat), .individual(.weight):
throw VitalHealthKitClientError.invalidRemappedResource
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ struct QuantityUnit {

if #available(iOS 16.0, *) {
mapping[.appleSleepingWristTemperature] = .degreeCelsius
mapping[.atrialFibrillationBurden] = .percentage
}

mapping[.bodyMass] = .kg
Expand Down Expand Up @@ -289,6 +290,7 @@ struct QuantityUnit {

if #available(iOS 16.0, *) {
mapping[.appleSleepingWristTemperature] = .degreeCelsius()
mapping[.atrialFibrillationBurden] = .percent()
}

mapping[.bodyMass] = .gramUnit(with: .kilo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,25 @@ func toHealthKitTypes(resource: VitalResource) -> HealthKitObjectTypeRequirement
],
supplementary: []
)

case .electrocardiogram:
return single(HKElectrocardiogramType.electrocardiogramType())
case .afibBurden:
if #available(iOS 16, *) {
return single(HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)!)
} else {
return HealthKitObjectTypeRequirements(required: [], optional: [], supplementary: [])
}
case .heartRateAlert:
return HealthKitObjectTypeRequirements(
required: [],
optional: [
HKCategoryType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!,
HKCategoryType.categoryType(forIdentifier: .highHeartRateEvent)!,
HKCategoryType.categoryType(forIdentifier: .lowHeartRateEvent)!,
],
supplementary: []
)
}
}

Expand Down Expand Up @@ -306,6 +325,14 @@ func observedSampleTypes() -> [[HKSampleType]] {
])
}

var afibBurdenTypes = [HKSampleType]()

if #available(iOS 16.0, *) {
afibBurdenTypes = [
HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)!
]
}

return [
/// Profile
[
Expand Down Expand Up @@ -441,6 +468,21 @@ func observedSampleTypes() -> [[HKSampleType]] {
[
HKSampleType.quantityType(forIdentifier: .respiratoryRate)!
],

/// AFib Burden
afibBurdenTypes,

/// Electrocardiogram
[
HKElectrocardiogramType.electrocardiogramType()
],

/// Heart rate alerts
[
HKCategoryType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!,
HKCategoryType.categoryType(forIdentifier: .highHeartRateEvent)!,
HKCategoryType.categoryType(forIdentifier: .lowHeartRateEvent)!,
]
]
}

Expand Down