MockPumpManager.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. //
  2. // MockPumpManager.swift
  3. // LoopKit
  4. //
  5. // Created by Michael Pangburn on 11/20/18.
  6. // Copyright © 2018 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. import LoopKit
  10. import LoopTestingKit
  11. public protocol MockPumpManagerStateObserver {
  12. func mockPumpManager(_ manager: MockPumpManager, didUpdate state: MockPumpManagerState)
  13. func mockPumpManager(_ manager: MockPumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus)
  14. }
  15. private enum MockPumpManagerError: LocalizedError {
  16. case pumpSuspended
  17. case communicationFailure
  18. case bolusInProgress
  19. var failureReason: String? {
  20. switch self {
  21. case .pumpSuspended:
  22. return "Pump is suspended"
  23. case .communicationFailure:
  24. return "Unable to communicate with pump"
  25. case .bolusInProgress:
  26. return "Bolus in progress"
  27. }
  28. }
  29. }
  30. public final class MockPumpManager: TestingPumpManager {
  31. public static let managerIdentifier = "MockPumpManager"
  32. public static let localizedTitle = "Simulator"
  33. private static let device = HKDevice(
  34. name: MockPumpManager.managerIdentifier,
  35. manufacturer: nil,
  36. model: nil,
  37. hardwareVersion: nil,
  38. firmwareVersion: nil,
  39. softwareVersion: String(LoopKitVersionNumber),
  40. localIdentifier: nil,
  41. udiDeviceIdentifier: nil
  42. )
  43. private static let deliveryUnitsPerMinute = 1.5
  44. private static let pulsesPerUnit: Double = 20
  45. private static let pumpReservoirCapacity: Double = 200
  46. public var pumpReservoirCapacity: Double {
  47. return MockPumpManager.pumpReservoirCapacity
  48. }
  49. public var reservoirFillFraction: Double {
  50. get {
  51. return state.reservoirUnitsRemaining / pumpReservoirCapacity
  52. }
  53. set {
  54. state.reservoirUnitsRemaining = newValue * pumpReservoirCapacity
  55. }
  56. }
  57. public var supportedBolusVolumes: [Double] {
  58. return supportedBasalRates
  59. }
  60. public var supportedBasalRates: [Double] {
  61. return (0...700).map { Double($0) / Double(type(of: self).pulsesPerUnit) }
  62. }
  63. public var maximumBasalScheduleEntryCount: Int {
  64. return 48
  65. }
  66. public var minimumBasalScheduleEntryDuration: TimeInterval {
  67. return .minutes(30)
  68. }
  69. public var testingDevice: HKDevice {
  70. return type(of: self).device
  71. }
  72. public var lastReconciliation: Date? {
  73. return Date()
  74. }
  75. private func basalDeliveryState(for state: MockPumpManagerState) -> PumpManagerStatus.BasalDeliveryState {
  76. if case .suspended(let date) = state.suspendState {
  77. return .suspended(date)
  78. }
  79. if let temp = state.unfinalizedTempBasal, !temp.finished {
  80. return .tempBasal(DoseEntry(temp))
  81. }
  82. if case .resumed(let date) = state.suspendState {
  83. return .active(date)
  84. } else {
  85. return .active(Date())
  86. }
  87. }
  88. private func bolusState(for state: MockPumpManagerState) -> PumpManagerStatus.BolusState {
  89. if let bolus = state.unfinalizedBolus, !bolus.finished {
  90. return .inProgress(DoseEntry(bolus))
  91. } else {
  92. return .none
  93. }
  94. }
  95. private func status(for state: MockPumpManagerState) -> PumpManagerStatus {
  96. return PumpManagerStatus(
  97. timeZone: .current,
  98. device: MockPumpManager.device,
  99. pumpBatteryChargeRemaining: state.pumpBatteryChargeRemaining,
  100. basalDeliveryState: basalDeliveryState(for: state),
  101. bolusState: .none)
  102. }
  103. public var pumpBatteryChargeRemaining: Double? {
  104. get {
  105. return state.pumpBatteryChargeRemaining
  106. }
  107. set {
  108. state.pumpBatteryChargeRemaining = newValue
  109. }
  110. }
  111. public var status: PumpManagerStatus {
  112. get {
  113. return status(for: self.state)
  114. }
  115. }
  116. private func notifyObservers() {
  117. }
  118. public var state: MockPumpManagerState {
  119. didSet {
  120. let newValue = state
  121. let oldStatus = status(for: oldValue)
  122. let newStatus = status(for: newValue)
  123. if oldStatus != newStatus {
  124. statusObservers.forEach { $0.pumpManager(self, didUpdate: newStatus, oldStatus: oldStatus) }
  125. }
  126. stateObservers.forEach { $0.mockPumpManager(self, didUpdate: self.state) }
  127. delegate.notify { (delegate) in
  128. if newValue.reservoirUnitsRemaining != oldValue.reservoirUnitsRemaining {
  129. delegate?.pumpManager(self, didReadReservoirValue: self.state.reservoirUnitsRemaining, at: Date()) { result in
  130. // nothing to do here
  131. }
  132. }
  133. delegate?.pumpManagerDidUpdateState(self)
  134. }
  135. }
  136. }
  137. public var pumpManagerDelegate: PumpManagerDelegate? {
  138. get {
  139. return delegate.delegate
  140. }
  141. set {
  142. delegate.delegate = newValue
  143. }
  144. }
  145. public var delegateQueue: DispatchQueue! {
  146. get {
  147. return delegate.queue
  148. }
  149. set {
  150. delegate.queue = newValue
  151. }
  152. }
  153. private let delegate = WeakSynchronizedDelegate<PumpManagerDelegate>()
  154. private var statusObservers = WeakSynchronizedSet<PumpManagerStatusObserver>()
  155. private var stateObservers = WeakSynchronizedSet<MockPumpManagerStateObserver>()
  156. public init() {
  157. state = MockPumpManagerState(
  158. reservoirUnitsRemaining: MockPumpManager.pumpReservoirCapacity,
  159. tempBasalEnactmentShouldError: false,
  160. bolusEnactmentShouldError: false,
  161. deliverySuspensionShouldError: false,
  162. deliveryResumptionShouldError: false,
  163. maximumBolus: 25.0,
  164. maximumBasalRatePerHour: 5.0,
  165. suspendState: .resumed(Date()),
  166. pumpBatteryChargeRemaining: 1,
  167. unfinalizedBolus: nil,
  168. unfinalizedTempBasal: nil,
  169. finalizedDoses: [])
  170. }
  171. public init?(rawState: RawStateValue) {
  172. guard let state = (rawState["state"] as? MockPumpManagerState.RawValue).flatMap(MockPumpManagerState.init(rawValue:)) else {
  173. return nil
  174. }
  175. self.state = state
  176. }
  177. public var rawState: RawStateValue {
  178. return ["state": state.rawValue]
  179. }
  180. public func createBolusProgressReporter(reportingOn dispatchQueue: DispatchQueue) -> DoseProgressReporter? {
  181. if case .inProgress(let dose) = status.bolusState {
  182. return MockDoseProgressEstimator(reportingQueue: dispatchQueue, dose: dose)
  183. }
  184. return nil
  185. }
  186. public var pumpRecordsBasalProfileStartEvents: Bool {
  187. return false
  188. }
  189. public func addStatusObserver(_ observer: PumpManagerStatusObserver, queue: DispatchQueue) {
  190. statusObservers.insert(observer, queue: queue)
  191. }
  192. public func addStateObserver(_ observer: MockPumpManagerStateObserver, queue: DispatchQueue) {
  193. stateObservers.insert(observer, queue: queue)
  194. }
  195. public func removeStatusObserver(_ observer: PumpManagerStatusObserver) {
  196. statusObservers.removeElement(observer)
  197. }
  198. public func assertCurrentPumpData() {
  199. state.finalizeFinishedDoses()
  200. storeDoses { (error) in
  201. self.delegate.notify { (delegate) in
  202. delegate?.pumpManagerRecommendsLoop(self)
  203. }
  204. guard error == nil else {
  205. return
  206. }
  207. DispatchQueue.main.async {
  208. let totalInsulinUsage = self.state.finalizedDoses.reduce(into: 0 as Double) { total, dose in
  209. total += dose.units
  210. }
  211. self.state.finalizedDoses = []
  212. self.state.reservoirUnitsRemaining -= totalInsulinUsage
  213. }
  214. }
  215. }
  216. private func storeDoses(completion: @escaping (_ error: Error?) -> Void) {
  217. state.finalizeFinishedDoses()
  218. let pendingPumpEvents = state.dosesToStore.map { NewPumpEvent($0) }
  219. delegate.notify { (delegate) in
  220. delegate?.pumpManager(self, hasNewPumpEvents: pendingPumpEvents, lastReconciliation: self.lastReconciliation) { error in
  221. completion(error)
  222. }
  223. }
  224. }
  225. public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerResult<DoseEntry>) -> Void) {
  226. if state.tempBasalEnactmentShouldError {
  227. completion(.failure(PumpManagerError.communication(MockPumpManagerError.communicationFailure)))
  228. } else {
  229. let now = Date()
  230. if let temp = state.unfinalizedTempBasal, temp.finishTime.compare(now) == .orderedDescending {
  231. state.unfinalizedTempBasal?.cancel(at: now)
  232. }
  233. state.finalizeFinishedDoses()
  234. if duration < .ulpOfOne {
  235. // Cancel temp basal
  236. let temp = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: now, duration: duration)
  237. storeDoses { (error) in
  238. completion(.success(DoseEntry(temp)))
  239. }
  240. } else {
  241. let temp = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: now, duration: duration)
  242. state.unfinalizedTempBasal = temp
  243. storeDoses { (error) in
  244. completion(.success(DoseEntry(temp)))
  245. }
  246. }
  247. }
  248. }
  249. public func enactBolus(units: Double, at startDate: Date, willRequest: @escaping (DoseEntry) -> Void, completion: @escaping (PumpManagerResult<DoseEntry>) -> Void) {
  250. if state.bolusEnactmentShouldError {
  251. completion(.failure(SetBolusError.certain(PumpManagerError.communication(MockPumpManagerError.communicationFailure))))
  252. } else {
  253. state.finalizeFinishedDoses()
  254. if let _ = state.unfinalizedBolus {
  255. completion(.failure(SetBolusError.certain(PumpManagerError.deviceState(MockPumpManagerError.bolusInProgress))))
  256. return
  257. }
  258. if case .suspended = status.basalDeliveryState {
  259. completion(.failure(SetBolusError.certain(PumpManagerError.deviceState(MockPumpManagerError.pumpSuspended))))
  260. return
  261. }
  262. let bolus = UnfinalizedDose(bolusAmount: units, startTime: Date(), duration: .minutes(units / type(of: self).deliveryUnitsPerMinute))
  263. let dose = DoseEntry(bolus)
  264. willRequest(dose)
  265. state.unfinalizedBolus = bolus
  266. storeDoses { (error) in
  267. completion(.success(dose))
  268. }
  269. }
  270. }
  271. public func cancelBolus(completion: @escaping (PumpManagerResult<DoseEntry?>) -> Void) {
  272. state.unfinalizedBolus?.cancel(at: Date())
  273. storeDoses { (_) in
  274. DispatchQueue.main.async {
  275. self.state.finalizeFinishedDoses()
  276. completion(.success(nil))
  277. }
  278. }
  279. }
  280. public func setMustProvideBLEHeartbeat(_ mustProvideBLEHeartbeat: Bool) {
  281. // nothing to do here
  282. }
  283. public func suspendDelivery(completion: @escaping (Error?) -> Void) {
  284. if self.state.deliverySuspensionShouldError {
  285. completion(PumpManagerError.communication(MockPumpManagerError.communicationFailure))
  286. } else {
  287. let now = Date()
  288. state.unfinalizedTempBasal?.cancel(at: now)
  289. state.unfinalizedBolus?.cancel(at: now)
  290. let suspendDate = Date()
  291. let suspend = UnfinalizedDose(suspendStartTime: suspendDate)
  292. self.state.finalizedDoses.append(suspend)
  293. self.state.suspendState = .suspended(suspendDate)
  294. storeDoses { (error) in
  295. completion(error)
  296. }
  297. }
  298. }
  299. public func resumeDelivery(completion: @escaping (Error?) -> Void) {
  300. if self.state.deliveryResumptionShouldError {
  301. completion(PumpManagerError.communication(MockPumpManagerError.communicationFailure))
  302. } else {
  303. let resumeDate = Date()
  304. let resume = UnfinalizedDose(resumeStartTime: resumeDate)
  305. self.state.finalizedDoses.append(resume)
  306. self.state.suspendState = .resumed(resumeDate)
  307. storeDoses { (error) in
  308. completion(error)
  309. }
  310. }
  311. }
  312. public func injectPumpEvents(_ pumpEvents: [NewPumpEvent]) {
  313. state.finalizedDoses += pumpEvents.compactMap { $0.unfinalizedDose }
  314. }
  315. }
  316. extension MockPumpManager {
  317. public var debugDescription: String {
  318. return """
  319. ## MockPumpManager
  320. status: \(status)
  321. state: \(state)
  322. stateObservers.count: \(stateObservers.cleanupDeallocatedElements().count)
  323. statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)
  324. """
  325. }
  326. }