| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- //
- // MockPumpManager.swift
- // LoopKit
- //
- // Created by Michael Pangburn on 11/20/18.
- // Copyright © 2018 LoopKit Authors. All rights reserved.
- //
- import HealthKit
- import LoopKit
- import LoopTestingKit
- public protocol MockPumpManagerStateObserver {
- func mockPumpManager(_ manager: MockPumpManager, didUpdate state: MockPumpManagerState)
- func mockPumpManager(_ manager: MockPumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus)
- }
- public enum MockPumpManagerError: LocalizedError {
- case pumpSuspended
- case communicationFailure
- case bolusInProgress
- case missingSettings
- case pumpError
-
- public var failureReason: String? {
- switch self {
- case .pumpSuspended:
- return "Pump is suspended"
- case .communicationFailure:
- return "Unable to communicate with pump"
- case .bolusInProgress:
- return "Bolus in progress"
- case .missingSettings:
- return "Missing Settings"
- case .pumpError:
- return "Pump is in an error state"
- }
- }
- }
- public final class MockPumpManager: TestingPumpManager {
- public static let managerIdentifier = "MockPumpManager"
- public var managerIdentifier: String {
- return MockPumpManager.managerIdentifier
- }
-
- public static let localizedTitle = "Insulin Pump Simulator"
-
- public var localizedTitle: String {
- return MockPumpManager.localizedTitle
- }
- public static var onboardingMaximumBasalScheduleEntryCount: Int {
- return 48
- }
- public static var onboardingSupportedBasalRates: [Double] {
- MockPumpManagerState.DeliverableIncrements.medtronicX22.supportedBasalRates!
- }
- public static var onboardingSupportedBolusVolumes: [Double] {
- MockPumpManagerState.DeliverableIncrements.medtronicX22.supportedBolusVolumes!
- }
- public static var onboardingSupportedMaximumBolusVolumes: [Double] {
- self.onboardingSupportedBolusVolumes
- }
- private static let device = HKDevice(
- name: MockPumpManager.managerIdentifier,
- manufacturer: nil,
- model: nil,
- hardwareVersion: nil,
- firmwareVersion: nil,
- softwareVersion: String(LoopKitVersionNumber),
- localIdentifier: nil,
- udiDeviceIdentifier: nil
- )
- private static let deliveryUnitsPerMinute = 1.5
- private static let pumpReservoirCapacity: Double = 200
- public var pumpReservoirCapacity: Double {
- return MockPumpManager.pumpReservoirCapacity
- }
- public var reservoirFillFraction: Double {
- get {
- return state.reservoirUnitsRemaining / pumpReservoirCapacity
- }
- set {
- state.reservoirUnitsRemaining = max(newValue * pumpReservoirCapacity, 0)
- }
- }
- public var currentBasalRate: HKQuantity? {
- switch status.basalDeliveryState {
- case .suspending, .suspended(_):
- return HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 0)
- case .tempBasal(let dose):
- return HKQuantity(unit: .internationalUnitsPerHour, doubleValue: dose.unitsPerHour)
- case .none:
- return nil
- default:
- guard let scheduledBasalRate = state.basalRateSchedule?.value(at: Date()) else { return nil }
- return HKQuantity(unit: .internationalUnitsPerHour, doubleValue: scheduledBasalRate)
- }
- }
- public var supportedBolusVolumes: [Double] {
- return state.supportedBolusVolumes
- }
- public var supportedMaximumBolusVolumes: [Double] {
- state.supportedBolusVolumes
- }
- public var supportedBasalRates: [Double] {
- return state.supportedBasalRates
- }
- public var maximumBasalScheduleEntryCount: Int {
- return 48
- }
- public var minimumBasalScheduleEntryDuration: TimeInterval {
- return .minutes(30)
- }
- public var testingDevice: HKDevice {
- return type(of: self).device
- }
- public var testLastReconciliation: Date? = nil
-
- public var lastSync: Date? {
- return testLastReconciliation ?? Date()
- }
-
- public var insulinType: InsulinType? {
- return state.insulinType
- }
- private func basalDeliveryState(for state: MockPumpManagerState) -> PumpManagerStatus.BasalDeliveryState? {
- if case .suspended(let date) = state.suspendState {
- return .suspended(date)
- }
- if state.occlusionDetected || state.pumpErrorDetected || state.pumpBatteryChargeRemaining == 0 || state.reservoirUnitsRemaining == 0 {
- return nil
- }
- if let temp = state.unfinalizedTempBasal, !temp.finished {
- return .tempBasal(DoseEntry(temp))
- }
- if case .resumed(let date) = state.suspendState {
- return .active(date)
- } else {
- return .active(Date())
- }
- }
- private func bolusState(for state: MockPumpManagerState) -> PumpManagerStatus.BolusState {
- if let bolus = state.unfinalizedBolus, !bolus.finished {
- return .inProgress(DoseEntry(bolus))
- } else {
- return .noBolus
- }
- }
-
- public func buildPumpStatusHighlight(for state: MockPumpManagerState) -> PumpStatusHighlight? {
- if state.deliveryIsUncertain {
- return PumpStatusHighlight(localizedMessage: NSLocalizedString("Comms Issue", comment: "Status highlight that delivery is uncertain."),
- imageName: "exclamationmark.circle.fill",
- state: .critical)
- }
- else if state.reservoirUnitsRemaining == 0 {
- return PumpStatusHighlight(localizedMessage: NSLocalizedString("No Insulin", comment: "Status highlight that a pump is out of insulin."),
- imageName: "exclamationmark.circle.fill",
- state: .critical)
- } else if state.occlusionDetected {
- return PumpStatusHighlight(localizedMessage: NSLocalizedString("Pump Occlusion", comment: "Status highlight that an occlusion was detected."),
- imageName: "exclamationmark.circle.fill",
- state: .critical)
- } else if state.pumpErrorDetected {
- return PumpStatusHighlight(localizedMessage: NSLocalizedString("Pump Error", comment: "Status highlight that a pump error occurred."),
- imageName: "exclamationmark.circle.fill",
- state: .critical)
- } else if pumpBatteryChargeRemaining == 0 {
- return PumpStatusHighlight(localizedMessage: NSLocalizedString("Pump Battery Dead", comment: "Status highlight that pump has a dead battery."),
- imageName: "exclamationmark.circle.fill",
- state: .critical)
- } else if case .suspended = state.suspendState {
- return PumpStatusHighlight(localizedMessage: NSLocalizedString("Insulin Suspended", comment: "Status highlight that insulin delivery was suspended."),
- imageName: "pause.circle.fill",
- state: .warning)
- }
-
- return nil
- }
-
- public func buildPumpLifecycleProgress(for state: MockPumpManagerState) -> PumpLifecycleProgress? {
- guard let progressPercentComplete = state.progressPercentComplete else {
- return nil
- }
-
- let progressState: DeviceLifecycleProgressState
- if let progressCriticalThresholdPercentValue = state.progressCriticalThresholdPercentValue,
- progressPercentComplete >= progressCriticalThresholdPercentValue
- {
- progressState = .critical
- } else if let progressWarningThresholdPercentValue = state.progressWarningThresholdPercentValue,
- progressPercentComplete >= progressWarningThresholdPercentValue
- {
- progressState = .warning
- } else {
- progressState = .normalPump
- }
-
- return PumpLifecycleProgress(percentComplete: progressPercentComplete,
- progressState: progressState)
- }
-
- public var isClockOffset: Bool {
- let now = Date()
- return TimeZone.current.secondsFromGMT(for: now) != state.timeZone.secondsFromGMT(for: now)
- }
- private func status(for state: MockPumpManagerState) -> PumpManagerStatus {
- return PumpManagerStatus(
- timeZone: state.timeZone,
- device: MockPumpManager.device,
- pumpBatteryChargeRemaining: state.pumpBatteryChargeRemaining,
- basalDeliveryState: basalDeliveryState(for: state),
- bolusState: bolusState(for: state),
- insulinType: state.insulinType,
- deliveryIsUncertain: state.deliveryIsUncertain
- )
- }
- public var pumpBatteryChargeRemaining: Double? {
- get {
- return state.pumpBatteryChargeRemaining
- }
- set {
- state.pumpBatteryChargeRemaining = newValue
- }
- }
- public var status: PumpManagerStatus {
- get {
- return status(for: self.state)
- }
- }
-
- private func notifyStatusObservers(oldStatus: PumpManagerStatus) {
- let status = self.status
- delegate.notify { (delegate) in
- delegate?.pumpManager(self, didUpdate: status, oldStatus: oldStatus)
- }
- statusObservers.forEach { (observer) in
- observer.pumpManager(self, didUpdate: status, oldStatus: oldStatus)
- }
- }
- public var state: MockPumpManagerState {
- didSet {
- let newValue = state
- guard newValue != oldValue else {
- return
- }
- let oldStatus = status(for: oldValue)
- let newStatus = status(for: newValue)
- if oldStatus != newStatus {
- notifyStatusObservers(oldStatus: oldStatus)
- }
-
- // stop insulin delivery as pump state requires
- if (newValue.occlusionDetected != oldValue.occlusionDetected && newValue.occlusionDetected) ||
- (newValue.pumpErrorDetected != oldValue.pumpErrorDetected && newValue.pumpErrorDetected) ||
- (newValue.pumpBatteryChargeRemaining != oldValue.pumpBatteryChargeRemaining && newValue.pumpBatteryChargeRemaining == 0) ||
- (newValue.reservoirUnitsRemaining != oldValue.reservoirUnitsRemaining && newValue.reservoirUnitsRemaining == 0)
- {
- stopInsulinDelivery()
- }
-
- stateObservers.forEach { $0.mockPumpManager(self, didUpdate: self.state) }
- delegate.notify { (delegate) in
- if newValue.reservoirUnitsRemaining != oldValue.reservoirUnitsRemaining {
- delegate?.pumpManager(self, didReadReservoirValue: self.state.reservoirUnitsRemaining, at: Date()) { result in
- // nothing to do here
- }
- }
- delegate?.pumpManagerDidUpdateState(self)
-
- delegate?.pumpManager(self, didUpdate: newStatus, oldStatus: oldStatus)
- }
- }
- }
- public var pumpManagerDelegate: PumpManagerDelegate? {
- get {
- return delegate.delegate
- }
- set {
- delegate.delegate = newValue
- }
- }
- public var delegateQueue: DispatchQueue! {
- get {
- return delegate.queue
- }
- set {
- delegate.queue = newValue
- }
- }
- private let delegate = WeakSynchronizedDelegate<PumpManagerDelegate>()
- private var statusObservers = WeakSynchronizedSet<PumpManagerStatusObserver>()
- private var stateObservers = WeakSynchronizedSet<MockPumpManagerStateObserver>()
- public init() {
- state = MockPumpManagerState(reservoirUnitsRemaining: MockPumpManager.pumpReservoirCapacity)
- }
- public init?(rawState: RawStateValue) {
- if let state = (rawState["state"] as? MockPumpManagerState.RawValue).flatMap(MockPumpManagerState.init(rawValue:)) {
- self.state = state
- } else {
- self.state = MockPumpManagerState(reservoirUnitsRemaining: MockPumpManager.pumpReservoirCapacity)
- }
- }
- public var rawState: RawStateValue {
- return ["state": state.rawValue]
- }
-
- public let isOnboarded = true // No distinction between created and onboarded
- private func logDeviceCommunication(_ message: String, type: DeviceLogEntryType = .send) {
- self.delegate.delegate?.deviceManager(self, logEventForDeviceIdentifier: "MockId", type: type, message: message, completion: nil)
- }
- public func createBolusProgressReporter(reportingOn dispatchQueue: DispatchQueue) -> DoseProgressReporter? {
- if case .inProgress(let dose) = status.bolusState {
- return MockDoseProgressEstimator(reportingQueue: dispatchQueue, dose: dose)
- }
- return nil
- }
- public var pumpRecordsBasalProfileStartEvents: Bool {
- return false
- }
- public func addStatusObserver(_ observer: PumpManagerStatusObserver, queue: DispatchQueue) {
- statusObservers.insert(observer, queue: queue)
- }
- public func addStateObserver(_ observer: MockPumpManagerStateObserver, queue: DispatchQueue) {
- stateObservers.insert(observer, queue: queue)
- }
- public func removeStatusObserver(_ observer: PumpManagerStatusObserver) {
- statusObservers.removeElement(observer)
- }
- public func ensureCurrentPumpData(completion: ((Date?) -> Void)?) {
- // Change this to artificially increase the delay fetching the current pump data
- let fetchDelay = 0
- DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(fetchDelay)) {
-
- self.state.finalizeFinishedDoses()
-
- self.storePumpEvents { (error) in
- guard error == nil else {
- completion?(self.lastSync)
- return
- }
-
- let totalInsulinUsage = self.state.finalizedDoses.reduce(into: 0 as Double) { total, dose in
- total += dose.units
- }
-
- self.state.finalizedDoses = []
- self.state.reservoirUnitsRemaining = max(self.state.reservoirUnitsRemaining - totalInsulinUsage, 0)
-
- completion?(self.lastSync)
- }
- }
- }
- private func storePumpEvents(completion: @escaping (_ error: Error?) -> Void) {
- state.finalizeFinishedDoses()
- let pendingPumpEvents = state.pumpEventsToStore
- delegate.notify { (delegate) in
- delegate?.pumpManager(self, hasNewPumpEvents: pendingPumpEvents, lastReconciliation: self.lastSync) { error in
- if error == nil {
- self.state.additionalPumpEvents = []
- }
- completion(error)
- }
- }
- }
- public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerError?) -> Void) {
- logDeviceComms(.send, message: "Temp Basal \(unitsPerHour) U/hr Duration:\(duration.hours)")
-
- if state.tempBasalEnactmentShouldError || state.pumpBatteryChargeRemaining == 0 {
- let error = PumpManagerError.communication(MockPumpManagerError.communicationFailure)
- logDeviceComms(.error, message: "Temp Basal failed with error \(error)")
- completion(error)
- } else if state.deliveryCommandsShouldTriggerUncertainDelivery {
- state.deliveryIsUncertain = true
- logDeviceComms(.error, message: "Uncertain delivery for temp basal")
- completion(.uncertainDelivery)
- } else if state.occlusionDetected || state.pumpErrorDetected {
- let error = PumpManagerError.deviceState(MockPumpManagerError.pumpError)
- logDeviceComms(.error, message: "Temp Basal failed because the pump is in an error state")
- completion(error)
- } else if case .suspended = state.suspendState {
- let error = PumpManagerError.deviceState(MockPumpManagerError.pumpSuspended)
- logDeviceComms(.error, message: "Temp Basal failed because inulin delivery is suspended")
- completion(error)
- } else if state.reservoirUnitsRemaining == 0 {
- let error = PumpManagerError.deviceState(MockPumpManagerError.pumpSuspended)
- logDeviceComms(.error, message: "Temp Basal failed because there is no insulin in the reservoir")
- completion(error)
- } else {
- let now = Date()
- if let temp = state.unfinalizedTempBasal, temp.finishTime.compare(now) == .orderedDescending {
- state.unfinalizedTempBasal?.cancel(at: now)
- }
- state.finalizeFinishedDoses()
- logDeviceComms(.receive, message: "Temp Basal succeeded")
- if duration < .ulpOfOne {
- // Cancel temp basal
- storePumpEvents { (error) in
- completion(nil)
- }
- } else {
- let temp = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: now, duration: duration, insulinType: state.insulinType)
- state.unfinalizedTempBasal = temp
- storePumpEvents { (error) in
- completion(nil)
- }
- }
- logDeviceCommunication("enactTempBasal succeeded", type: .receive)
- }
- }
-
- private func logDeviceComms(_ type: DeviceLogEntryType, message: String) {
- self.delegate.delegate?.deviceManager(self, logEventForDeviceIdentifier: "mockpump", type: type, message: message, completion: nil)
- }
- public func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (PumpManagerError?) -> Void) {
- logDeviceCommunication("enactBolus(\(units), \(activationType))")
- if state.bolusEnactmentShouldError || state.pumpBatteryChargeRemaining == 0 {
- let error = PumpManagerError.communication(MockPumpManagerError.communicationFailure)
- logDeviceComms(.error, message: "Bolus failed with error \(error)")
- completion(error)
- } else if state.deliveryCommandsShouldTriggerUncertainDelivery {
- state.deliveryIsUncertain = true
- logDeviceComms(.error, message: "Uncertain delivery for bolus")
- completion(PumpManagerError.uncertainDelivery)
- } else if state.occlusionDetected || state.pumpErrorDetected {
- let error = PumpManagerError.deviceState(MockPumpManagerError.pumpError)
- logDeviceComms(.error, message: "Bolus failed because the pump is in an error state")
- completion(error)
- } else if state.reservoirUnitsRemaining == 0 {
- let error = PumpManagerError.deviceState(MockPumpManagerError.pumpSuspended)
- logDeviceComms(.error, message: "Bolus failed because there is no insulin in the reservoir")
- completion(error)
- } else {
- state.finalizeFinishedDoses()
- if let _ = state.unfinalizedBolus {
- logDeviceCommunication("enactBolus failed: bolusInProgress", type: .error)
- completion(PumpManagerError.deviceState(MockPumpManagerError.bolusInProgress))
- return
- }
- if case .suspended = status.basalDeliveryState {
- logDeviceCommunication("enactBolus failed: pumpSuspended", type: .error)
- completion(PumpManagerError.deviceState(MockPumpManagerError.pumpSuspended))
- return
- }
-
-
- let bolus = UnfinalizedDose(bolusAmount: units, startTime: Date(), duration: .minutes(units / type(of: self).deliveryUnitsPerMinute), insulinType: state.insulinType, automatic: activationType.isAutomatic)
- state.unfinalizedBolus = bolus
-
- logDeviceComms(.receive, message: "Bolus accepted")
-
- storePumpEvents { (error) in
- completion(nil)
- self.logDeviceCommunication("enactBolus succeeded", type: .receive)
- }
- }
- }
- public func cancelBolus(completion: @escaping (PumpManagerResult<DoseEntry?>) -> Void) {
- logDeviceComms(.send, message: "Cancel")
-
- if self.state.bolusCancelShouldError {
- let error = PumpManagerError.communication(MockPumpManagerError.communicationFailure)
- logDeviceComms(.error, message: "Cancel failed with error: \(error)")
- completion(.failure(error))
- } else {
- state.unfinalizedBolus?.cancel(at: Date())
-
- storePumpEvents { (_) in
- DispatchQueue.main.async {
- self.state.finalizeFinishedDoses()
- completion(.success(nil))
- }
- }
- }
- }
- public func setMustProvideBLEHeartbeat(_ mustProvideBLEHeartbeat: Bool) {
- // nothing to do here
- }
-
- private func stopInsulinDelivery() {
- let now = Date()
- state.unfinalizedTempBasal?.cancel(at: now)
- state.unfinalizedBolus?.cancel(at: now)
- storePumpEvents { _ in }
- }
- public func suspendDelivery(completion: @escaping (Error?) -> Void) {
- logDeviceComms(.send, message: "Suspend")
-
- if self.state.deliverySuspensionShouldError {
- let error = PumpManagerError.communication(MockPumpManagerError.communicationFailure)
- logDeviceComms(.error, message: "Suspend failed with error: \(error)")
- completion(error)
- } else {
- let now = Date()
- state.unfinalizedTempBasal?.cancel(at: now)
- state.unfinalizedBolus?.cancel(at: now)
- let suspendDate = Date()
- let suspend = UnfinalizedDose(suspendStartTime: suspendDate)
- self.state.finalizedDoses.append(suspend)
- self.state.suspendState = .suspended(suspendDate)
- logDeviceComms(.receive, message: "Suspend accepted")
- storePumpEvents { (error) in
- completion(error)
- }
- logDeviceCommunication("suspendDelivery succeeded", type: .receive)
- }
- }
- public func resumeDelivery(completion: @escaping (Error?) -> Void) {
- logDeviceComms(.send, message: "Resume")
- if self.state.deliveryResumptionShouldError {
- let error = PumpManagerError.communication(MockPumpManagerError.communicationFailure)
- logDeviceComms(.error, message: "Resume failed with error: \(error)")
- completion(error)
- } else {
- let resumeDate = Date()
- let resume = UnfinalizedDose(resumeStartTime: resumeDate, insulinType: state.insulinType)
- self.state.finalizedDoses.append(resume)
- self.state.suspendState = .resumed(resumeDate)
- storePumpEvents { (error) in
- completion(error)
- }
- logDeviceCommunication("resumeDelivery succeeded", type: .receive)
- }
- }
- public func injectPumpEvents(_ pumpEvents: [NewPumpEvent]) {
- state.finalizedDoses += pumpEvents.compactMap { $0.unfinalizedDose }
- state.additionalPumpEvents += pumpEvents.filter { $0.dose == nil }
- }
-
- public func setMaximumTempBasalRate(_ rate: Double) { }
- public func syncBasalRateSchedule(items scheduleItems: [RepeatingScheduleValue<Double>], completion: @escaping (Result<BasalRateSchedule, Error>) -> Void) {
- state.basalRateSchedule = BasalRateSchedule(dailyItems: scheduleItems, timeZone: self.status.timeZone)
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
- completion(.success(BasalRateSchedule(dailyItems: scheduleItems, timeZone: self.status.timeZone)!))
- }
- }
- public func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (Result<DeliveryLimits, Error>) -> Void) {
- DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
- completion(.success(deliveryLimits))
- }
- }
- }
- // MARK: - AlertResponder implementation
- extension MockPumpManager {
- public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) {
- completion(nil)
- }
- }
- // MARK: - AlertSoundVendor implementation
- extension MockPumpManager {
- public func getSoundBaseURL() -> URL? { return nil }
- public func getSounds() -> [Alert.Sound] { return [] }
- }
- extension MockPumpManager {
- public var debugDescription: String {
- return """
- ## MockPumpManager
- status: \(status)
- state: \(state)
- stateObservers.count: \(stateObservers.cleanupDeallocatedElements().count)
- statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)
- """
- }
- }
|