PumpManager.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. //
  2. // PumpManager.swift
  3. // Loop
  4. //
  5. // Copyright © 2018 LoopKit Authors. All rights reserved.
  6. //
  7. import Foundation
  8. import HealthKit
  9. public enum PumpManagerResult<T> {
  10. case success(T)
  11. case failure(PumpManagerError)
  12. }
  13. public protocol PumpManagerStatusObserver: AnyObject {
  14. func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus)
  15. }
  16. public enum BolusActivationType: String, Codable {
  17. case manualNoRecommendation
  18. case manualRecommendationAccepted
  19. case manualRecommendationChanged
  20. case automatic
  21. public var isAutomatic: Bool {
  22. self == .automatic
  23. }
  24. static public func activationTypeFor(recommendedAmount: Double?, bolusAmount: Double) -> BolusActivationType {
  25. guard let recommendedAmount = recommendedAmount else { return .manualNoRecommendation }
  26. return recommendedAmount =~ bolusAmount ? .manualRecommendationAccepted : .manualRecommendationChanged
  27. }
  28. }
  29. public protocol PumpManagerDelegate: DeviceManagerDelegate, PumpManagerStatusObserver {
  30. func pumpManagerBLEHeartbeatDidFire(_ pumpManager: PumpManager)
  31. /// Queries the delegate as to whether Loop requires the pump to provide its own periodic scheduling
  32. /// via BLE.
  33. /// This is the companion to `PumpManager.setMustProvideBLEHeartbeat(_:)`
  34. func pumpManagerMustProvideBLEHeartbeat(_ pumpManager: PumpManager) -> Bool
  35. /// Informs the delegate that the manager is deactivating and should be deleted
  36. func pumpManagerWillDeactivate(_ pumpManager: PumpManager)
  37. /// Informs the delegate that the hardware this PumpManager has been reporting for has been replaced.
  38. func pumpManagerPumpWasReplaced(_ pumpManager: PumpManager)
  39. /// Triggered when pump model changes. With a more formalized setup flow (which requires a successful model fetch),
  40. /// this delegate method could go away.
  41. func pumpManager(_ pumpManager: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents pumpRecordsBasalProfileStartEvents: Bool)
  42. /// Reports an error that should be surfaced to the user
  43. func pumpManager(_ pumpManager: PumpManager, didError error: PumpManagerError)
  44. /// This should be called any time the PumpManager synchronizes with the pump, even if there are no new doses in the log, as changes to lastReconciliation
  45. /// indicate we can trust insulin delivery status up until that point, even if there are no new doses.
  46. /// For pumps whose only source of dosing adjustments is Loop, lastReconciliation should be reflective of the last time we received telemetry from the pump.
  47. /// For pumps with a user interface and dosing history capabilities, lastReconciliation should be reflective of the last time we reconciled fully with pump history, and know
  48. /// that we have accounted for any external doses. It is possible for the pump to report reservoir data beyond the date of lastReconciliation, and Loop can use it for
  49. /// calculating IOB.
  50. func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void)
  51. func pumpManager(_ pumpManager: PumpManager, didReadReservoirValue units: Double, at date: Date, completion: @escaping (_ result: Result<(newValue: ReservoirValue, lastValue: ReservoirValue?, areStoredValuesContinuous: Bool), Error>) -> Void)
  52. func pumpManager(_ pumpManager: PumpManager, didAdjustPumpClockBy adjustment: TimeInterval)
  53. func pumpManagerDidUpdateState(_ pumpManager: PumpManager)
  54. func startDateToFilterNewPumpEvents(for manager: PumpManager) -> Date
  55. /// Indicates the system time offset from a trusted time source. If the return value is added to the system time, the result is the trusted time source value. If the trusted time source is earlier than the system time, the return value is negative.
  56. var detectedSystemTimeOffset: TimeInterval { get }
  57. }
  58. public protocol PumpManager: DeviceManager {
  59. /// The maximum number of scheduled basal rates in a single day supported by the pump. Used during onboarding by therapy settings.
  60. static var onboardingMaximumBasalScheduleEntryCount: Int { get }
  61. /// All user-selectable basal rates, in Units per Hour. Must be non-empty. Used during onboarding by therapy settings.
  62. static var onboardingSupportedBasalRates: [Double] { get }
  63. /// All user-selectable bolus volumes, in Units. Must be non-empty. Used during onboarding by therapy settings.
  64. static var onboardingSupportedBolusVolumes: [Double] { get }
  65. /// All user-selectable maximum bolus volumes, in Units. Must be non-empty. Used during onboarding by therapy settings.
  66. static var onboardingSupportedMaximumBolusVolumes: [Double] { get }
  67. /// The queue on which PumpManagerDelegate methods are called
  68. /// Setting to nil resets to a default provided by the manager
  69. var delegateQueue: DispatchQueue! { get set }
  70. /// Rounds a basal rate in U/hr to a rate supported by this pump.
  71. ///
  72. /// - Parameters:
  73. /// - unitsPerHour: A desired rate of delivery in Units per Hour
  74. /// - Returns: The rounded rate of delivery in Units per Hour. The rate returned should not be larger than the passed in rate.
  75. func roundToSupportedBasalRate(unitsPerHour: Double) -> Double
  76. /// Rounds a bolus volume in Units to a volume supported by this pump.
  77. ///
  78. /// - Parameters:
  79. /// - units: A desired volume of delivery in Units
  80. /// - Returns: The rounded bolus volume in Units. The volume returned should not be larger than the passed in rate.
  81. func roundToSupportedBolusVolume(units: Double) -> Double
  82. /// All user-selectable basal rates, in Units per Hour. Must be non-empty.
  83. var supportedBasalRates: [Double] { get }
  84. /// All user-selectable bolus volumes, in Units. Must be non-empty.
  85. var supportedBolusVolumes: [Double] { get }
  86. /// All user-selectable bolus volumes for setting the maximum allowed bolus, in Units. Must be non-empty.
  87. var supportedMaximumBolusVolumes: [Double] { get }
  88. /// The maximum number of scheduled basal rates in a single day supported by the pump
  89. var maximumBasalScheduleEntryCount: Int { get }
  90. /// The basal schedule duration increment, beginning at midnight, supported by the pump
  91. var minimumBasalScheduleEntryDuration: TimeInterval { get }
  92. /// The primary client receiving notifications about the pump lifecycle
  93. /// All delegate methods are called on `delegateQueue`
  94. var pumpManagerDelegate: PumpManagerDelegate? { get set }
  95. /// Whether the PumpManager provides DoseEntry values for scheduled basal delivery. If false, Loop will use the basal schedule to infer normal basal delivery during times not overridden by:
  96. /// - Temporary basal delivery
  97. /// - Suspend/Resume pairs
  98. /// - Rewind/Prime pairs
  99. var pumpRecordsBasalProfileStartEvents: Bool { get }
  100. /// The maximum reservoir volume of the pump
  101. var pumpReservoirCapacity: Double { get }
  102. /// The time of the last sync with the pump's event history, or reservoir, or last status check if pump does not provide history.
  103. var lastSync: Date? { get }
  104. /// The most-recent status
  105. var status: PumpManagerStatus { get }
  106. /// Adds an observer of changes in PumpManagerStatus
  107. ///
  108. /// Observers are held by weak reference.
  109. ///
  110. /// - Parameters:
  111. /// - observer: The observing object
  112. /// - queue: The queue on which the observer methods should be called
  113. func addStatusObserver(_ observer: PumpManagerStatusObserver, queue: DispatchQueue)
  114. /// Removes an observer of changes in PumpManagerStatus
  115. ///
  116. /// Since observers are held weakly, calling this method is not required when the observer is deallocated
  117. ///
  118. /// - Parameter observer: The observing object
  119. func removeStatusObserver(_ observer: PumpManagerStatusObserver)
  120. /// Ensure that the pump's data (reservoir/events) is up to date. If not, fetch it.
  121. /// The PumpManager should call the completion block with the date of last sync with the pump, nil if no sync has occurred
  122. func ensureCurrentPumpData(completion: ((_ lastSync: Date?) -> Void)?)
  123. /// Loop calls this method when the current environment requires the pump to provide its own periodic
  124. /// scheduling via BLE.
  125. /// The manager may choose to still enable its own heartbeat even if `mustProvideBLEHeartbeat` is false
  126. func setMustProvideBLEHeartbeat(_ mustProvideBLEHeartbeat: Bool)
  127. /// Returns a dose estimator for the current bolus, if one is in progress
  128. func createBolusProgressReporter(reportingOn dispatchQueue: DispatchQueue) -> DoseProgressReporter?
  129. /// Send a bolus command and handle the result
  130. ///
  131. /// - Parameters:
  132. /// - units: The number of units to deliver
  133. /// - automatic: Whether the dose was triggered automatically as opposed to commanded by user
  134. /// - completion: A closure called after the command is complete
  135. /// - error: An optional error describing why the command failed
  136. func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (_ error: PumpManagerError?) -> Void)
  137. /// Cancels the current, in progress, bolus.
  138. ///
  139. /// - Parameters:
  140. /// - completion: A closure called after the command is complete
  141. /// - result: A DoseEntry containing the actual delivery amount of the canceled bolus, nil if canceled bolus information is not available, or an error describing why the command failed.
  142. func cancelBolus(completion: @escaping (_ result: PumpManagerResult<DoseEntry?>) -> Void)
  143. /// Send a temporary basal rate command and handle the result
  144. ///
  145. /// - Parameters:
  146. /// - unitsPerHour: The temporary basal rate to set
  147. /// - duration: The duration of the temporary basal rate. If you pass in a duration of 0, that cancels any currently running Temp Basal
  148. /// - completion: A closure called after the command is complete
  149. /// - error: An optional error describing why the command failed
  150. func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (_ error: PumpManagerError?) -> Void)
  151. /// Send a command to the pump to suspend delivery
  152. ///
  153. /// - Parameters:
  154. /// - completion: A closure called after the command is complete
  155. /// - error: An error describing why the command failed
  156. func suspendDelivery(completion: @escaping (_ error: Error?) -> Void)
  157. /// Send a command to the pump to resume delivery
  158. ///
  159. /// - Parameters:
  160. /// - completion: A closure called after the command is complete
  161. /// - error: An error describing why the command failed
  162. func resumeDelivery(completion: @escaping (_ error: Error?) -> Void)
  163. /// Sync the schedule of basal rates to the pump, annotating the result with the proper time zone.
  164. ///
  165. /// - Precondition:
  166. /// - `scheduleItems` must not be empty.
  167. ///
  168. /// - Parameters:
  169. /// - scheduleItems: The items comprising the basal rate schedule
  170. /// - completion: A closure called after the command is complete
  171. /// - result: A BasalRateSchedule or an error describing why the command failed
  172. func syncBasalRateSchedule(items scheduleItems: [RepeatingScheduleValue<Double>], completion: @escaping (_ result: Result<BasalRateSchedule, Error>) -> Void)
  173. /// Sync the delivery limits for basal rate and bolus. If the pump does not support setting max bolus or max basal rates, the completion should be called with success including the provided delivery limits.
  174. ///
  175. /// - Parameters:
  176. /// - deliveryLimits: The delivery limits
  177. /// - completion: A closure called after the command is complete
  178. /// - result: The delivery limits set or an error describing why the command failed
  179. func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (_ result: Result<DeliveryLimits, Error>) -> Void)
  180. }
  181. public extension PumpManager {
  182. func roundToSupportedBasalRate(unitsPerHour: Double) -> Double {
  183. return supportedBasalRates.filter({$0 <= unitsPerHour}).max() ?? 0
  184. }
  185. func roundToSupportedBolusVolume(units: Double) -> Double {
  186. return supportedBolusVolumes.filter({$0 <= units}).max() ?? 0
  187. }
  188. /// Convenience wrapper for notifying the delegate of deactivation on the delegate queue
  189. ///
  190. /// - Parameters:
  191. /// - completion: A closure called from the delegate queue after the delegate is called
  192. func notifyDelegateOfDeactivation(completion: @escaping () -> Void) {
  193. delegateQueue.async {
  194. self.pumpManagerDelegate?.pumpManagerWillDeactivate(self)
  195. completion()
  196. }
  197. }
  198. }