// // MockPumpManagerState.swift // MockKit // // Created by Pete Schwamb on 7/31/19. // Copyright © 2019 LoopKit Authors. All rights reserved. // import Foundation import LoopKit public struct MockPumpManagerState: Equatable { public enum DeliverableIncrements: String, CaseIterable { case omnipod case medtronicX22 case medtronicX23 case custom var supportedBolusVolumes: [Double]? { switch self { case .omnipod: // 0.05 units for volumes between 0.05-30U return (1...600).map { Double($0) * 0.05 } case .medtronicX22: // 0.1 units for volumes between 0.1-25U return (1...250).map { Double($0) * 0.1 } case .medtronicX23: let breakpoints = [0, 1, 10, 25] let scales = [40, 20, 10] let scalingGroups = zip(scales, breakpoints.adjacentPairs().map(...)) return scalingGroups.flatMap { (scale, range) -> [Double] in let scaledRanges = (range.lowerBound * scale + 1)...(range.upperBound * scale) return scaledRanges.map { Double($0) / Double(scale) } } case .custom: return nil } } var bolusVolumesDescription: String? { switch self { case .omnipod: // 0.05 units for volumes between 0.05-30U return "0.05-30 by 0.05" case .medtronicX22: // 0.1 units for volumes between 0.1-25U return "0.1-25 by 0.1" case .medtronicX23: // 0.025 units for rates between 0.0-0.975 U/h // 0.05 units for rates between 1-9.95 U/h // 0.1 units for rates between 10-25 U/h return "0-1-10-25 by 0.025|0.05|0.1" case .custom: return nil } } var supportedBasalRates: [Double]? { switch self { case .omnipod: // 0.05 units for rates between 0.05-30U/hr return (1...600).map { Double($0) / 20 } case .medtronicX22: // 0.05 units for rates between 0.0-35U/hr return (0...700).map { Double($0) / 20 } case .medtronicX23: // 0.025 units for rates between 0.0-0.975 U/h let rateGroup1 = (0...39).map { Double($0) / 40 } // 0.05 units for rates between 1-9.95 U/h let rateGroup2 = (20...199).map { Double($0) / 20 } // 0.1 units for rates between 10-35 U/h let rateGroup3 = (100...350).map { Double($0) / 10 } return rateGroup1 + rateGroup2 + rateGroup3 case .custom: return nil } } public var basalRateDescription: String? { switch self { case .omnipod: // 0.05 units for rates between 0.05-30U/hr return "0.05-30 by 0.05" case .medtronicX22: // 0.05 units for rates between 0.0-35U/hr return "0-35 by 0.05" case .medtronicX23: // 0.025 units for rates between 0.0-0.975 U/h // 0.05 units for rates between 1-9.95 U/h // 0.1 units for rates between 10-35 U/h return "0-1-10-35 by 0.025|0.05|0.1" case .custom: return nil } } } public var deliverableIncrements: DeliverableIncrements { didSet { if let supportedBasalRates = deliverableIncrements.supportedBasalRates { self.supportedBasalRates = supportedBasalRates } else if let minBasalRates = supportedBasalRates.first, let maxBasalRates = supportedBasalRates.last, supportedBasalRates.indices.contains(1) { let stepSize = supportedBasalRates[1]-minBasalRates self.supportedBasalRates = (Int(minBasalRates/stepSize)...Int(maxBasalRates/stepSize)).map { Double($0) / (1 / stepSize) } } if let supportedBolusVolumes = deliverableIncrements.supportedBolusVolumes { self.supportedBolusVolumes = supportedBolusVolumes } else if let minBolusVolumes = supportedBolusVolumes.first, let maxBolusVolumes = supportedBolusVolumes.last, supportedBolusVolumes.indices.contains(1) { let stepSize = supportedBolusVolumes[1]-minBolusVolumes self.supportedBolusVolumes = (Int(minBolusVolumes/stepSize)...Int(maxBolusVolumes/stepSize)).map { Double($0) / (1 / stepSize) } } } } public var supportedBolusVolumes: [Double] public var supportedBolusVolumesDescription: String { guard let bolusVolumesDescription = deliverableIncrements.bolusVolumesDescription else { guard let minBolusVolume = supportedBolusVolumes.first, let maxBolusVolume = supportedBolusVolumes.last, supportedBolusVolumes.indices.contains(1) else { return "–" } return String(format: "\(minBolusVolume)-\(maxBolusVolume) by %.3f", (supportedBolusVolumes[1]-minBolusVolume)) } return bolusVolumesDescription } public var supportedBasalRates: [Double] public var supportedBasalRatesDescription: String { guard let basalRatesDescription = deliverableIncrements.basalRateDescription else { guard let minBasalRate = supportedBasalRates.first, let maxBasalRate = supportedBasalRates.last, supportedBasalRates.indices.contains(1) else { return "–" } return String(format: "\(minBasalRate)-\(maxBasalRate) by %.3f", (supportedBasalRates[1]-minBasalRate)) } return basalRatesDescription } public var basalRateSchedule: BasalRateSchedule? public var reservoirUnitsRemaining: Double public var tempBasalEnactmentShouldError: Bool public var bolusEnactmentShouldError: Bool public var bolusCancelShouldError: Bool public var deliverySuspensionShouldError: Bool public var deliveryResumptionShouldError: Bool public var deliveryCommandsShouldTriggerUncertainDelivery: Bool public var maximumBolus: Double public var maximumBasalRatePerHour: Double public var suspendState: SuspendState public var pumpBatteryChargeRemaining: Double? public var occlusionDetected: Bool = false public var pumpErrorDetected: Bool = false public var deliveryIsUncertain: Bool = false public var unfinalizedBolus: UnfinalizedDose? public var unfinalizedTempBasal: UnfinalizedDose? var finalizedDoses: [UnfinalizedDose] var additionalPumpEvents: [NewPumpEvent] public var progressPercentComplete: Double? public var progressWarningThresholdPercentValue: Double? public var progressCriticalThresholdPercentValue: Double? public var insulinType: InsulinType? public var timeZone: TimeZone public var dosesToStore: [UnfinalizedDose] { return finalizedDoses + [unfinalizedTempBasal, unfinalizedBolus].compactMap {$0} } public var pumpEventsToStore: [NewPumpEvent] { return dosesToStore.map { NewPumpEvent($0) } + additionalPumpEvents } public init(deliverableIncrements: DeliverableIncrements = .medtronicX22, reservoirUnitsRemaining: Double = 200.0, tempBasalEnactmentShouldError: Bool = false, bolusEnactmentShouldError: Bool = false, bolusCancelShouldError: Bool = false, deliverySuspensionShouldError: Bool = false, deliveryResumptionShouldError: Bool = false, deliveryCommandsShouldTriggerUncertainDelivery: Bool = false, maximumBolus: Double = 25.0, maximumBasalRatePerHour: Double = 5.0, suspendState: SuspendState = .resumed(Date()), pumpBatteryChargeRemaining: Double? = 1, unfinalizedBolus: UnfinalizedDose? = nil, unfinalizedTempBasal: UnfinalizedDose? = nil, finalizedDoses: [UnfinalizedDose] = [], additionalPumpEvents: [NewPumpEvent] = [], progressWarningThresholdPercentValue: Double? = 0.75, progressCriticalThresholdPercentValue: Double? = 0.9, insulinType: InsulinType = .novolog) { self.deliverableIncrements = deliverableIncrements self.supportedBolusVolumes = deliverableIncrements.supportedBolusVolumes ?? [] self.supportedBasalRates = deliverableIncrements.supportedBasalRates ?? [] self.reservoirUnitsRemaining = reservoirUnitsRemaining self.tempBasalEnactmentShouldError = tempBasalEnactmentShouldError self.bolusEnactmentShouldError = bolusEnactmentShouldError self.bolusCancelShouldError = bolusCancelShouldError self.deliverySuspensionShouldError = deliverySuspensionShouldError self.deliveryResumptionShouldError = deliveryResumptionShouldError self.deliveryCommandsShouldTriggerUncertainDelivery = deliveryCommandsShouldTriggerUncertainDelivery self.maximumBolus = maximumBolus self.maximumBasalRatePerHour = maximumBasalRatePerHour self.suspendState = suspendState self.pumpBatteryChargeRemaining = pumpBatteryChargeRemaining self.finalizedDoses = finalizedDoses self.additionalPumpEvents = additionalPumpEvents self.progressWarningThresholdPercentValue = progressWarningThresholdPercentValue self.progressCriticalThresholdPercentValue = progressCriticalThresholdPercentValue self.insulinType = insulinType self.timeZone = .currentFixed } public mutating func finalizeFinishedDoses() { if let bolus = unfinalizedBolus, bolus.finished { finalizedDoses.append(bolus) unfinalizedBolus = nil } if let tempBasal = unfinalizedTempBasal, tempBasal.finished { finalizedDoses.append(tempBasal) unfinalizedTempBasal = nil } } } extension MockPumpManagerState: RawRepresentable { public typealias RawValue = [String: Any] public init?(rawValue: RawValue) { guard let reservoirUnitsRemaining = rawValue["reservoirUnitsRemaining"] as? Double else { return nil } let defaultDeliverableIncrements: DeliverableIncrements = .medtronicX22 self.deliverableIncrements = (rawValue["deliverableIncrements"] as? DeliverableIncrements.RawValue).flatMap(DeliverableIncrements.init(rawValue:)) ?? defaultDeliverableIncrements self.supportedBolusVolumes = rawValue["supportedBolusVolumes"] as? [Double] ?? defaultDeliverableIncrements.supportedBolusVolumes ?? [] self.supportedBasalRates = rawValue["supportedBasalRates"] as? [Double] ?? defaultDeliverableIncrements.supportedBasalRates ?? [] self.reservoirUnitsRemaining = reservoirUnitsRemaining self.tempBasalEnactmentShouldError = rawValue["tempBasalEnactmentShouldError"] as? Bool ?? false self.bolusEnactmentShouldError = rawValue["bolusEnactmentShouldError"] as? Bool ?? false self.bolusCancelShouldError = rawValue["bolusCancelShouldError"] as? Bool ?? false self.deliverySuspensionShouldError = rawValue["deliverySuspensionShouldError"] as? Bool ?? false self.deliveryResumptionShouldError = rawValue["deliveryResumptionShouldError"] as? Bool ?? false self.deliveryCommandsShouldTriggerUncertainDelivery = rawValue["deliveryCommandsShouldTriggerUncertainDelivery"] as? Bool ?? false self.maximumBolus = rawValue["maximumBolus"] as? Double ?? 25.0 self.maximumBasalRatePerHour = rawValue["maximumBasalRatePerHour"] as? Double ?? 5.0 self.pumpBatteryChargeRemaining = rawValue["pumpBatteryChargeRemaining"] as? Double ?? nil self.occlusionDetected = rawValue["occlusionDetected"] as? Bool ?? false self.pumpErrorDetected = rawValue["pumpErrorDetected"] as? Bool ?? false self.deliveryIsUncertain = rawValue["deliveryIsUncertain"] as? Bool ?? false self.progressPercentComplete = rawValue["progressPercentComplete"] as? Double self.progressWarningThresholdPercentValue = rawValue["progressWarningThresholdPercentValue"] as? Double self.progressCriticalThresholdPercentValue = rawValue["progressCriticalThresholdPercentValue"] as? Double if let rawBasalRateSchedule = rawValue["basalRateSchedule"] as? BasalRateSchedule.RawValue { self.basalRateSchedule = BasalRateSchedule(rawValue: rawBasalRateSchedule) } if let rawUnfinalizedBolus = rawValue["unfinalizedBolus"] as? UnfinalizedDose.RawValue { self.unfinalizedBolus = UnfinalizedDose(rawValue: rawUnfinalizedBolus) } if let rawUnfinalizedTempBasal = rawValue["unfinalizedTempBasal"] as? UnfinalizedDose.RawValue { self.unfinalizedTempBasal = UnfinalizedDose(rawValue: rawUnfinalizedTempBasal) } if let rawFinalizedDoses = rawValue["finalizedDoses"] as? [UnfinalizedDose.RawValue] { self.finalizedDoses = rawFinalizedDoses.compactMap( { UnfinalizedDose(rawValue: $0) } ) } else { self.finalizedDoses = [] } if let rawAdditionalPumpEvents = rawValue["additionalPumpEvents"] as? [NewPumpEvent.RawValue] { self.additionalPumpEvents = rawAdditionalPumpEvents.compactMap( { NewPumpEvent(rawValue: $0) } ) } else { self.additionalPumpEvents = [] } if let rawSuspendState = rawValue["suspendState"] as? SuspendState.RawValue, let suspendState = SuspendState(rawValue: rawSuspendState) { self.suspendState = suspendState } else { self.suspendState = .resumed(Date()) } if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) { self.insulinType = insulinType } else { self.insulinType = .novolog } if let timeZoneOffset = rawValue["timeZone"] as? Int { self.timeZone = TimeZone(secondsFromGMT: timeZoneOffset) ?? .currentFixed } else { self.timeZone = .currentFixed } } public var rawValue: RawValue { var raw: RawValue = [ "deliverableIncrements": deliverableIncrements.rawValue, "supportedBolusVolumes": supportedBolusVolumes, "supportedBasalRates": supportedBasalRates, "reservoirUnitsRemaining": reservoirUnitsRemaining, "timeZone": timeZone.secondsFromGMT() ] raw["basalRateSchedule"] = basalRateSchedule?.rawValue raw["suspendState"] = suspendState.rawValue if tempBasalEnactmentShouldError { raw["tempBasalEnactmentShouldError"] = true } if bolusEnactmentShouldError { raw["bolusEnactmentShouldError"] = true } if bolusCancelShouldError { raw["bolusCancelShouldError"] = true } if deliverySuspensionShouldError { raw["deliverySuspensionShouldError"] = true } if deliveryResumptionShouldError { raw["deliveryResumptionShouldError"] = true } if deliveryCommandsShouldTriggerUncertainDelivery { raw["deliveryCommandsShouldTriggerUncertainDelivery"] = true } if deliveryIsUncertain { raw["deliveryIsUncertain"] = true } raw["finalizedDoses"] = finalizedDoses.map({ $0.rawValue }) raw["additionalPumpEvents"] = additionalPumpEvents.map({ $0.rawValue }) raw["maximumBolus"] = maximumBolus raw["maximumBasalRatePerHour"] = maximumBasalRatePerHour raw["unfinalizedBolus"] = unfinalizedBolus?.rawValue raw["unfinalizedTempBasal"] = unfinalizedTempBasal?.rawValue raw["pumpBatteryChargeRemaining"] = pumpBatteryChargeRemaining raw["occlusionDetected"] = occlusionDetected raw["pumpErrorDetected"] = pumpErrorDetected raw["progressPercentComplete"] = progressPercentComplete raw["progressWarningThresholdPercentValue"] = progressWarningThresholdPercentValue raw["progressCriticalThresholdPercentValue"] = progressCriticalThresholdPercentValue raw["insulinType"] = insulinType?.rawValue return raw } } extension MockPumpManagerState: CustomDebugStringConvertible { public var debugDescription: String { return """ ## MockPumpManagerState * deliverableIncrements: \(deliverableIncrements) * reservoirUnitsRemaining: \(reservoirUnitsRemaining) * basalRateSchedule: \(basalRateSchedule as Any) * tempBasalEnactmentShouldError: \(tempBasalEnactmentShouldError) * bolusEnactmentShouldError: \(bolusEnactmentShouldError) * bolusCancelShouldError: \(bolusCancelShouldError) * deliverySuspensionShouldError: \(deliverySuspensionShouldError) * deliveryResumptionShouldError: \(deliveryResumptionShouldError) * maximumBolus: \(maximumBolus) * maximumBasalRatePerHour: \(maximumBasalRatePerHour) * pumpBatteryChargeRemaining: \(String(describing: pumpBatteryChargeRemaining)) * suspendState: \(suspendState) * unfinalizedBolus: \(String(describing: unfinalizedBolus)) * unfinalizedTempBasal: \(String(describing: unfinalizedTempBasal)) * finalizedDoses: \(finalizedDoses) * additionalPumpEvents: \(additionalPumpEvents) * occlusionDetected: \(occlusionDetected) * pumpErrorDetected: \(pumpErrorDetected) * progressPercentComplete: \(progressPercentComplete as Any) * progressWarningThresholdPercentValue: \(progressWarningThresholdPercentValue as Any) * progressCriticalThresholdPercentValue: \(progressCriticalThresholdPercentValue as Any) * insulinType: \(insulinType as Any) """ } } public enum SuspendState: Equatable, RawRepresentable { public typealias RawValue = [String: Any] private enum SuspendStateType: Int { case suspend, resume } case suspended(Date) case resumed(Date) public init?(rawValue: RawValue) { guard let suspendStateType = rawValue["suspendStateType"] as? SuspendStateType.RawValue, let date = rawValue["date"] as? Date else { return nil } switch SuspendStateType(rawValue: suspendStateType) { case .suspend?: self = .suspended(date) case .resume?: self = .resumed(date) default: return nil } } public var rawValue: RawValue { switch self { case .suspended(let date): return [ "suspendStateType": SuspendStateType.suspend.rawValue, "date": date ] case .resumed(let date): return [ "suspendStateType": SuspendStateType.resume.rawValue, "date": date ] } } }