HKQuantitySample+InsulinKit.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. //
  2. // HKQuantitySample+InsulinKit.swift
  3. // InsulinKit
  4. //
  5. // Copyright © 2017 LoopKit Authors. All rights reserved.
  6. //
  7. import HealthKit
  8. /// Defines the scheduled basal insulin rate during the time of the basal delivery sample
  9. let MetadataKeyScheduledBasalRate = "com.loopkit.InsulinKit.MetadataKeyScheduledBasalRate"
  10. /// Defines the programmed rate for a temporary basal dose
  11. let MetadataKeyProgrammedTempBasalRate = "com.loopkit.InsulinKit.MetadataKeyProgrammedTempBasalRate"
  12. /// A crude determination of whether a sample was written by LoopKit, in the case of multiple LoopKit-enabled app versions on the same phone.
  13. let MetadataKeyHasLoopKitOrigin = "HasLoopKitOrigin"
  14. /// Defines the insulin curve type to use to evaluate the dose's activity
  15. let MetadataKeyInsulinType = "com.loopkit.InsulinKit.MetadataKeyInsulinType"
  16. /// Defines the source of the data, including if a dose was logged or from device history
  17. let MetadataKeyProvenanceIdentifier = "com.loopkit.InsulinKit.MetadataKeyProvenanceIdentifier"
  18. /// Flag indicating whether this dose was issued automatically or if a user issued it manually.
  19. let MetadataKeyAutomaticallyIssued = "com.loopkit.InsulinKit.MetadataKeyAutomaticallyIssued"
  20. extension HKQuantitySample {
  21. convenience init?(type: HKQuantityType, unit: HKUnit, dose: DoseEntry, device: HKDevice?, provenanceIdentifier: String, syncVersion: Int = 1) {
  22. let units = dose.unitsInDeliverableIncrements
  23. guard let syncIdentifier = dose.syncIdentifier else {
  24. return nil
  25. }
  26. var metadata: [String: Any] = [
  27. HKMetadataKeySyncVersion: syncVersion,
  28. HKMetadataKeySyncIdentifier: syncIdentifier,
  29. MetadataKeyHasLoopKitOrigin: true,
  30. MetadataKeyProvenanceIdentifier: provenanceIdentifier
  31. ]
  32. switch dose.type {
  33. case .basal, .tempBasal, .suspend:
  34. // Ignore 0-duration basal entries
  35. guard dose.endDate.timeIntervalSince(dose.startDate) > .ulpOfOne else {
  36. return nil
  37. }
  38. metadata[HKMetadataKeyInsulinDeliveryReason] = HKInsulinDeliveryReason.basal.rawValue
  39. if let basalRate = dose.scheduledBasalRate {
  40. metadata[MetadataKeyScheduledBasalRate] = basalRate
  41. }
  42. if dose.type == .tempBasal {
  43. metadata[MetadataKeyProgrammedTempBasalRate] = HKQuantity(unit: .internationalUnitsPerHour, doubleValue: dose.unitsPerHour)
  44. }
  45. case .bolus:
  46. // Ignore 0-unit bolus entries
  47. guard units > .ulpOfOne else {
  48. return nil
  49. }
  50. metadata[HKMetadataKeyInsulinDeliveryReason] = HKInsulinDeliveryReason.bolus.rawValue
  51. case .resume:
  52. return nil
  53. }
  54. if let insulinType = dose.insulinType {
  55. metadata[MetadataKeyInsulinType] = insulinType.healthKitRepresentation
  56. }
  57. if let automatic = dose.automatic {
  58. metadata[MetadataKeyAutomaticallyIssued] = automatic
  59. }
  60. self.init(
  61. type: type,
  62. quantity: HKQuantity(unit: unit, doubleValue: units),
  63. start: dose.startDate,
  64. end: dose.endDate,
  65. device: device,
  66. metadata: metadata
  67. )
  68. }
  69. var hasLoopKitOrigin: Bool {
  70. guard let hasLoopKitOrigin = metadata?[MetadataKeyHasLoopKitOrigin] as? Bool else {
  71. return false
  72. }
  73. return hasLoopKitOrigin
  74. }
  75. var insulinDeliveryReason: HKInsulinDeliveryReason? {
  76. guard let reason = metadata?[HKMetadataKeyInsulinDeliveryReason] as? HKInsulinDeliveryReason.RawValue else {
  77. return nil
  78. }
  79. return HKInsulinDeliveryReason(rawValue: reason)
  80. }
  81. var scheduledBasalRate: HKQuantity? {
  82. return metadata?[MetadataKeyScheduledBasalRate] as? HKQuantity
  83. }
  84. var programmedTempBasalRate: HKQuantity? {
  85. return metadata?[MetadataKeyProgrammedTempBasalRate] as? HKQuantity
  86. }
  87. var loopSpecificProvenanceIdentifier: String {
  88. return metadata?[MetadataKeyProvenanceIdentifier] as? String ?? provenanceIdentifier
  89. }
  90. var automaticallyIssued: Bool? {
  91. return metadata?[MetadataKeyAutomaticallyIssued] as? Bool
  92. }
  93. var insulinType: InsulinType? {
  94. guard let rawType = metadata?[MetadataKeyInsulinType] as? String else {
  95. return nil
  96. }
  97. return InsulinType(healthKitRepresentation: rawType)
  98. }
  99. /// Returns a DoseEntry representation of the sample.
  100. /// Doses are not normalized, nor should they be assumed reconciled.
  101. var dose: DoseEntry? {
  102. guard let reason = insulinDeliveryReason else {
  103. return nil
  104. }
  105. let type: DoseType
  106. let scheduledBasalRate = self.scheduledBasalRate
  107. switch reason {
  108. case .basal:
  109. if scheduledBasalRate == nil {
  110. type = .basal
  111. } else {
  112. type = .tempBasal
  113. }
  114. // We can't properly trust non-LoopKit-provided basal insulin
  115. guard hasLoopKitOrigin else {
  116. return nil
  117. }
  118. case .bolus:
  119. type = .bolus
  120. @unknown default:
  121. return nil
  122. }
  123. let value: Double
  124. let unit: DoseUnit
  125. let deliveredUnits: Double?
  126. if let programmedRate = programmedTempBasalRate {
  127. value = programmedRate.doubleValue(for: .internationalUnitsPerHour)
  128. unit = .unitsPerHour
  129. deliveredUnits = quantity.doubleValue(for: .internationalUnit())
  130. } else {
  131. value = quantity.doubleValue(for: .internationalUnit())
  132. unit = .units
  133. deliveredUnits = nil
  134. }
  135. return DoseEntry(
  136. type: type,
  137. startDate: startDate,
  138. endDate: endDate,
  139. value: value,
  140. unit: unit,
  141. deliveredUnits: deliveredUnits,
  142. description: nil,
  143. syncIdentifier: syncIdentifier,
  144. scheduledBasalRate: scheduledBasalRate,
  145. insulinType: insulinType,
  146. automatic: automaticallyIssued
  147. )
  148. }
  149. }
  150. enum InsulinTypeHealthKitRepresentation: String {
  151. case novolog = "Novolog"
  152. case humalog = "Humalog"
  153. case apidra = "Apidra"
  154. case fiasp = "Fiasp"
  155. case lyumjev = "Lyumjev"
  156. }
  157. extension InsulinType {
  158. var healthKitRepresentation: String {
  159. switch self {
  160. case .novolog:
  161. return InsulinTypeHealthKitRepresentation.novolog.rawValue
  162. case .humalog:
  163. return InsulinTypeHealthKitRepresentation.humalog.rawValue
  164. case .apidra:
  165. return InsulinTypeHealthKitRepresentation.apidra.rawValue
  166. case .fiasp:
  167. return InsulinTypeHealthKitRepresentation.fiasp.rawValue
  168. case .lyumjev:
  169. return InsulinTypeHealthKitRepresentation.lyumjev.rawValue
  170. }
  171. }
  172. init?(healthKitRepresentation: String) {
  173. switch healthKitRepresentation {
  174. case InsulinTypeHealthKitRepresentation.novolog.rawValue:
  175. self = .novolog
  176. case InsulinTypeHealthKitRepresentation.humalog.rawValue:
  177. self = .humalog
  178. case InsulinTypeHealthKitRepresentation.apidra.rawValue:
  179. self = .apidra
  180. case InsulinTypeHealthKitRepresentation.fiasp.rawValue:
  181. self = .fiasp
  182. case InsulinTypeHealthKitRepresentation.lyumjev.rawValue:
  183. self = .lyumjev
  184. default:
  185. return nil
  186. }
  187. }
  188. }