| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- //
- // 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)
- }
- private enum MockPumpManagerError: LocalizedError {
- case pumpSuspended
- case communicationFailure
- case bolusInProgress
- 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"
- }
- }
- }
- public final class MockPumpManager: TestingPumpManager {
- public static let managerIdentifier = "MockPumpManager"
- public static let localizedTitle = "Simulator"
- 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 pulsesPerUnit: Double = 20
- 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 = newValue * pumpReservoirCapacity
- }
- }
- public var supportedBolusVolumes: [Double] {
- return supportedBasalRates
- }
- public var supportedBasalRates: [Double] {
- return (0...700).map { Double($0) / Double(type(of: self).pulsesPerUnit) }
- }
- public var maximumBasalScheduleEntryCount: Int {
- return 48
- }
- public var minimumBasalScheduleEntryDuration: TimeInterval {
- return .minutes(30)
- }
- public var testingDevice: HKDevice {
- return type(of: self).device
- }
- public var lastReconciliation: Date? {
- return Date()
- }
- private func basalDeliveryState(for state: MockPumpManagerState) -> PumpManagerStatus.BasalDeliveryState {
- if case .suspended(let date) = state.suspendState {
- return .suspended(date)
- }
- 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 .none
- }
- }
- private func status(for state: MockPumpManagerState) -> PumpManagerStatus {
- return PumpManagerStatus(
- timeZone: .current,
- device: MockPumpManager.device,
- pumpBatteryChargeRemaining: state.pumpBatteryChargeRemaining,
- basalDeliveryState: basalDeliveryState(for: state),
- bolusState: .none)
- }
- public var pumpBatteryChargeRemaining: Double? {
- get {
- return state.pumpBatteryChargeRemaining
- }
- set {
- state.pumpBatteryChargeRemaining = newValue
- }
- }
- public var status: PumpManagerStatus {
- get {
- return status(for: self.state)
- }
- }
- private func notifyObservers() {
- }
- public var state: MockPumpManagerState {
- didSet {
- let newValue = state
- let oldStatus = status(for: oldValue)
- let newStatus = status(for: newValue)
- if oldStatus != newStatus {
- statusObservers.forEach { $0.pumpManager(self, didUpdate: newStatus, oldStatus: oldStatus) }
- }
- 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)
- }
- }
- }
- 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,
- tempBasalEnactmentShouldError: false,
- bolusEnactmentShouldError: false,
- deliverySuspensionShouldError: false,
- deliveryResumptionShouldError: false,
- maximumBolus: 25.0,
- maximumBasalRatePerHour: 5.0,
- suspendState: .resumed(Date()),
- pumpBatteryChargeRemaining: 1,
- unfinalizedBolus: nil,
- unfinalizedTempBasal: nil,
- finalizedDoses: [])
- }
- public init?(rawState: RawStateValue) {
- guard let state = (rawState["state"] as? MockPumpManagerState.RawValue).flatMap(MockPumpManagerState.init(rawValue:)) else {
- return nil
- }
- self.state = state
- }
- public var rawState: RawStateValue {
- return ["state": state.rawValue]
- }
- 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 assertCurrentPumpData() {
- state.finalizeFinishedDoses()
- storeDoses { (error) in
- self.delegate.notify { (delegate) in
- delegate?.pumpManagerRecommendsLoop(self)
- }
- guard error == nil else {
- return
- }
- DispatchQueue.main.async {
- let totalInsulinUsage = self.state.finalizedDoses.reduce(into: 0 as Double) { total, dose in
- total += dose.units
- }
- self.state.finalizedDoses = []
- self.state.reservoirUnitsRemaining -= totalInsulinUsage
- }
- }
- }
- private func storeDoses(completion: @escaping (_ error: Error?) -> Void) {
- state.finalizeFinishedDoses()
- let pendingPumpEvents = state.dosesToStore.map { NewPumpEvent($0) }
- delegate.notify { (delegate) in
- delegate?.pumpManager(self, hasNewPumpEvents: pendingPumpEvents, lastReconciliation: self.lastReconciliation) { error in
- completion(error)
- }
- }
- }
- public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerResult<DoseEntry>) -> Void) {
- if state.tempBasalEnactmentShouldError {
- completion(.failure(PumpManagerError.communication(MockPumpManagerError.communicationFailure)))
- } else {
- let now = Date()
- if let temp = state.unfinalizedTempBasal, temp.finishTime.compare(now) == .orderedDescending {
- state.unfinalizedTempBasal?.cancel(at: now)
- }
- state.finalizeFinishedDoses()
- if duration < .ulpOfOne {
- // Cancel temp basal
- let temp = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: now, duration: duration)
- storeDoses { (error) in
- completion(.success(DoseEntry(temp)))
- }
- } else {
- let temp = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: now, duration: duration)
- state.unfinalizedTempBasal = temp
- storeDoses { (error) in
- completion(.success(DoseEntry(temp)))
- }
- }
- }
- }
- public func enactBolus(units: Double, at startDate: Date, willRequest: @escaping (DoseEntry) -> Void, completion: @escaping (PumpManagerResult<DoseEntry>) -> Void) {
- if state.bolusEnactmentShouldError {
- completion(.failure(SetBolusError.certain(PumpManagerError.communication(MockPumpManagerError.communicationFailure))))
- } else {
- state.finalizeFinishedDoses()
- if let _ = state.unfinalizedBolus {
- completion(.failure(SetBolusError.certain(PumpManagerError.deviceState(MockPumpManagerError.bolusInProgress))))
- return
- }
- if case .suspended = status.basalDeliveryState {
- completion(.failure(SetBolusError.certain(PumpManagerError.deviceState(MockPumpManagerError.pumpSuspended))))
- return
- }
- let bolus = UnfinalizedDose(bolusAmount: units, startTime: Date(), duration: .minutes(units / type(of: self).deliveryUnitsPerMinute))
- let dose = DoseEntry(bolus)
- willRequest(dose)
- state.unfinalizedBolus = bolus
- storeDoses { (error) in
- completion(.success(dose))
- }
- }
- }
- public func cancelBolus(completion: @escaping (PumpManagerResult<DoseEntry?>) -> Void) {
- state.unfinalizedBolus?.cancel(at: Date())
- storeDoses { (_) in
- DispatchQueue.main.async {
- self.state.finalizeFinishedDoses()
- completion(.success(nil))
- }
- }
- }
- public func setMustProvideBLEHeartbeat(_ mustProvideBLEHeartbeat: Bool) {
- // nothing to do here
- }
- public func suspendDelivery(completion: @escaping (Error?) -> Void) {
- if self.state.deliverySuspensionShouldError {
- completion(PumpManagerError.communication(MockPumpManagerError.communicationFailure))
- } 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)
- storeDoses { (error) in
- completion(error)
- }
- }
- }
- public func resumeDelivery(completion: @escaping (Error?) -> Void) {
- if self.state.deliveryResumptionShouldError {
- completion(PumpManagerError.communication(MockPumpManagerError.communicationFailure))
- } else {
- let resumeDate = Date()
- let resume = UnfinalizedDose(resumeStartTime: resumeDate)
- self.state.finalizedDoses.append(resume)
- self.state.suspendState = .resumed(resumeDate)
- storeDoses { (error) in
- completion(error)
- }
- }
- }
- public func injectPumpEvents(_ pumpEvents: [NewPumpEvent]) {
- state.finalizedDoses += pumpEvents.compactMap { $0.unfinalizedDose }
- }
- }
- extension MockPumpManager {
- public var debugDescription: String {
- return """
- ## MockPumpManager
- status: \(status)
- state: \(state)
- stateObservers.count: \(stateObservers.cleanupDeallocatedElements().count)
- statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)
- """
- }
- }
|