// // MockCGMManager.swift // LoopKit // // Created by Michael Pangburn on 11/20/18. // Copyright © 2018 LoopKit Authors. All rights reserved. // import HealthKit import LoopKit import LoopKitUI // TODO: DeviceStatusBadge references should live in MockKitUI import LoopTestingKit public struct MockCGMState: GlucoseDisplayable { public var isStateValid: Bool public var trendType: GlucoseTrend? public var trendRate: HKQuantity? public var isLocal: Bool { return true } public var glucoseRangeCategory: GlucoseRangeCategory? public let unit: HKUnit = .milligramsPerDeciliter public var glucoseAlertingEnabled: Bool public var samplesShouldBeUploaded: Bool private var cgmLowerLimitValue: Double // HKQuantity isn't codable public var cgmLowerLimit: HKQuantity { get { return HKQuantity.init(unit: unit, doubleValue: cgmLowerLimitValue) } set { var newDoubleValue = newValue.doubleValue(for: unit) if newDoubleValue >= urgentLowGlucoseThresholdValue { newDoubleValue = urgentLowGlucoseThresholdValue - 1 } cgmLowerLimitValue = newDoubleValue } } private var urgentLowGlucoseThresholdValue: Double public var urgentLowGlucoseThreshold: HKQuantity { get { return HKQuantity.init(unit: unit, doubleValue: urgentLowGlucoseThresholdValue) } set { var newDoubleValue = newValue.doubleValue(for: unit) if newDoubleValue <= cgmLowerLimitValue { newDoubleValue = cgmLowerLimitValue + 1 } if newDoubleValue >= lowGlucoseThresholdValue { newDoubleValue = lowGlucoseThresholdValue - 1 } urgentLowGlucoseThresholdValue = newDoubleValue } } private var lowGlucoseThresholdValue: Double public var lowGlucoseThreshold: HKQuantity { get { return HKQuantity.init(unit: unit, doubleValue: lowGlucoseThresholdValue) } set { var newDoubleValue = newValue.doubleValue(for: unit) if newDoubleValue <= urgentLowGlucoseThresholdValue { newDoubleValue = urgentLowGlucoseThresholdValue + 1 } if newDoubleValue >= highGlucoseThresholdValue { newDoubleValue = highGlucoseThresholdValue - 1 } lowGlucoseThresholdValue = newDoubleValue } } private var highGlucoseThresholdValue: Double public var highGlucoseThreshold: HKQuantity { get { return HKQuantity.init(unit: unit, doubleValue: highGlucoseThresholdValue) } set { var newDoubleValue = newValue.doubleValue(for: unit) if newDoubleValue <= lowGlucoseThresholdValue { newDoubleValue = lowGlucoseThresholdValue + 1 } if newDoubleValue >= cgmUpperLimitValue { newDoubleValue = cgmUpperLimitValue - 1 } highGlucoseThresholdValue = newDoubleValue } } private var cgmUpperLimitValue: Double public var cgmUpperLimit: HKQuantity { get { return HKQuantity.init(unit: unit, doubleValue: cgmUpperLimitValue) } set { var newDoubleValue = newValue.doubleValue(for: unit) if newDoubleValue <= highGlucoseThresholdValue { newDoubleValue = highGlucoseThresholdValue + 1 } cgmUpperLimitValue = newDoubleValue } } public var cgmStatusHighlight: MockCGMStatusHighlight? public var cgmStatusBadge: MockCGMStatusBadge? public var cgmLifecycleProgress: MockCGMLifecycleProgress? { didSet { if cgmLifecycleProgress != oldValue { setProgressColor() } } } public var progressWarningThresholdPercentValue: Double? { didSet { if progressWarningThresholdPercentValue != oldValue { setProgressColor() } } } public var progressCriticalThresholdPercentValue: Double? { didSet { if progressCriticalThresholdPercentValue != oldValue { setProgressColor() } } } public var cgmBatteryChargeRemaining: Double? = 1 private mutating func setProgressColor() { guard var cgmLifecycleProgress = cgmLifecycleProgress else { return } if let progressCriticalThresholdPercentValue = progressCriticalThresholdPercentValue, cgmLifecycleProgress.percentComplete >= progressCriticalThresholdPercentValue { cgmLifecycleProgress.progressState = .critical } else if let progressWarningThresholdPercentValue = progressWarningThresholdPercentValue, cgmLifecycleProgress.percentComplete >= progressWarningThresholdPercentValue { cgmLifecycleProgress.progressState = .warning } else { cgmLifecycleProgress.progressState = .normalCGM } self.cgmLifecycleProgress = cgmLifecycleProgress } public init(isStateValid: Bool = true, glucoseRangeCategory: GlucoseRangeCategory? = nil, glucoseAlertingEnabled: Bool = false, samplesShouldBeUploaded: Bool = false, urgentLowGlucoseThresholdValue: Double = 55, lowGlucoseThresholdValue: Double = 80, highGlucoseThresholdValue: Double = 200, cgmLowerLimitValue: Double = 40, cgmUpperLimitValue: Double = 400, cgmStatusHighlight: MockCGMStatusHighlight? = nil, cgmLifecycleProgress: MockCGMLifecycleProgress? = nil, progressWarningThresholdPercentValue: Double? = nil, progressCriticalThresholdPercentValue: Double? = nil) { self.isStateValid = isStateValid self.glucoseRangeCategory = glucoseRangeCategory self.glucoseAlertingEnabled = glucoseAlertingEnabled self.samplesShouldBeUploaded = samplesShouldBeUploaded self.urgentLowGlucoseThresholdValue = urgentLowGlucoseThresholdValue self.lowGlucoseThresholdValue = lowGlucoseThresholdValue self.highGlucoseThresholdValue = highGlucoseThresholdValue self.cgmLowerLimitValue = cgmLowerLimitValue self.cgmUpperLimitValue = cgmUpperLimitValue self.cgmStatusHighlight = cgmStatusHighlight self.cgmLifecycleProgress = cgmLifecycleProgress self.progressWarningThresholdPercentValue = progressWarningThresholdPercentValue self.progressCriticalThresholdPercentValue = progressCriticalThresholdPercentValue setProgressColor() } } public struct MockCGMStatusHighlight: DeviceStatusHighlight { public var localizedMessage: String public var imageName: String { switch alertIdentifier { case MockCGMManager.submarine.identifier: return "dot.radiowaves.left.and.right" case MockCGMManager.buzz.identifier: return "clock" default: return "exclamationmark.circle.fill" } } public var state: DeviceStatusHighlightState{ switch alertIdentifier { case MockCGMManager.submarine.identifier: return .normalCGM case MockCGMManager.buzz.identifier: return .warning default: return .critical } } public var alertIdentifier: Alert.AlertIdentifier } public struct MockCGMStatusBadge: DeviceStatusBadge { public var image: UIImage? { return badgeType.image } public var state: DeviceStatusBadgeState { switch badgeType { case .lowBattery: return .critical case .calibrationRequested: return .warning } } public var badgeType: MockCGMStatusBadgeType public enum MockCGMStatusBadgeType: Int, CaseIterable { case lowBattery case calibrationRequested var image: UIImage? { switch self { case .lowBattery: return UIImage(frameworkImage: "battery.circle.fill") case .calibrationRequested: return UIImage(frameworkImage: "drop.circle.fill") } } } init(badgeType: MockCGMStatusBadgeType) { self.badgeType = badgeType } } public struct MockCGMLifecycleProgress: DeviceLifecycleProgress, Equatable { public var percentComplete: Double public var progressState: DeviceLifecycleProgressState public init(percentComplete: Double, progressState: DeviceLifecycleProgressState = .normalCGM) { self.percentComplete = percentComplete self.progressState = progressState } } extension MockCGMLifecycleProgress: RawRepresentable { public typealias RawValue = [String: Any] public init?(rawValue: RawValue) { guard let percentComplete = rawValue["percentComplete"] as? Double, let progressStateRawValue = rawValue["progressState"] as? DeviceLifecycleProgressState.RawValue, let progressState = DeviceLifecycleProgressState(rawValue: progressStateRawValue) else { return nil } self.percentComplete = percentComplete self.progressState = progressState } public var rawValue: RawValue { let rawValue: RawValue = [ "percentComplete": percentComplete, "progressState": progressState.rawValue, ] return rawValue } } public final class MockCGMManager: TestingCGMManager { public static let managerIdentifier = "MockCGMManager" public var managerIdentifier: String { return MockCGMManager.managerIdentifier } public static let localizedTitle = "CGM Simulator" public var localizedTitle: String { return MockCGMManager.localizedTitle } public struct MockAlert { public let sound: Alert.Sound public let identifier: Alert.AlertIdentifier public let foregroundContent: Alert.Content public let backgroundContent: Alert.Content public let interruptionLevel: Alert.InterruptionLevel } let alerts: [Alert.AlertIdentifier: MockAlert] = [ submarine.identifier: submarine, buzz.identifier: buzz, critical.identifier: critical, signalLoss.identifier: signalLoss ] public static let submarine = MockAlert(sound: .sound(name: "sub.caf"), identifier: "submarine", foregroundContent: Alert.Content(title: "Alert: FG Title", body: "Alert: Foreground Body", acknowledgeActionButtonLabel: "FG OK"), backgroundContent: Alert.Content(title: "Alert: BG Title", body: "Alert: Background Body", acknowledgeActionButtonLabel: "BG OK"), interruptionLevel: .timeSensitive) public static let critical = MockAlert(sound: .sound(name: "critical.caf"), identifier: "critical", foregroundContent: Alert.Content(title: "Critical Alert: FG Title", body: "Critical Alert: Foreground Body", acknowledgeActionButtonLabel: "Critical FG OK"), backgroundContent: Alert.Content(title: "Critical Alert: BG Title", body: "Critical Alert: Background Body", acknowledgeActionButtonLabel: "Critical BG OK"), interruptionLevel: .critical) public static let buzz = MockAlert(sound: .vibrate, identifier: "buzz", foregroundContent: Alert.Content(title: "Alert: FG Title", body: "FG bzzzt", acknowledgeActionButtonLabel: "Buzz"), backgroundContent: Alert.Content(title: "Alert: BG Title", body: "BG bzzzt", acknowledgeActionButtonLabel: "Buzz"), interruptionLevel: .active) public static let signalLoss = MockAlert(sound: .sound(name: "critical.caf"), identifier: "signalLoss", foregroundContent: Alert.Content(title: "Signal Loss", body: "CGM simulator signal loss", acknowledgeActionButtonLabel: "Dismiss"), backgroundContent: Alert.Content(title: "Signal Loss", body: "CGM simulator signal loss", acknowledgeActionButtonLabel: "Dismiss"), interruptionLevel: .critical) private let lockedMockSensorState = Locked(MockCGMState(isStateValid: true)) public var mockSensorState: MockCGMState { get { lockedMockSensorState.value } set { lockedMockSensorState.mutate { $0 = newValue } self.notifyStatusObservers(cgmManagerStatus: self.cgmManagerStatus) } } public var glucoseDisplay: GlucoseDisplayable? { return mockSensorState } public var cgmManagerStatus: CGMManagerStatus { return CGMManagerStatus(hasValidSensorSession: dataSource.isValidSession, lastCommunicationDate: lastCommunicationDate, device: device) } private var lastCommunicationDate: Date? = nil public var testingDevice: HKDevice { return MockCGMDataSource.device } public var device: HKDevice? { return testingDevice } public weak var cgmManagerDelegate: CGMManagerDelegate? { get { return delegate.delegate } set { delegate.delegate = newValue } } public var delegateQueue: DispatchQueue! { get { return delegate.queue } set { delegate.queue = newValue } } private let delegate = WeakSynchronizedDelegate() private let lockedDataSource = Locked(MockCGMDataSource(model: .noData)) public var dataSource: MockCGMDataSource { get { lockedDataSource.value } set { lockedDataSource.mutate { $0 = newValue } self.notifyStatusObservers(cgmManagerStatus: self.cgmManagerStatus) } } private var glucoseUpdateTimer: Timer? public init() { setupGlucoseUpdateTimer() } // MARK: Handling CGM Manager Status observers private var statusObservers = WeakSynchronizedSet() public func addStatusObserver(_ observer: CGMManagerStatusObserver, queue: DispatchQueue) { statusObservers.insert(observer, queue: queue) } public func removeStatusObserver(_ observer: CGMManagerStatusObserver) { statusObservers.removeElement(observer) } private func notifyStatusObservers(cgmManagerStatus: CGMManagerStatus) { delegate.notify { delegate in delegate?.cgmManagerDidUpdateState(self) delegate?.cgmManager(self, didUpdate: self.cgmManagerStatus) } statusObservers.forEach { observer in observer.cgmManager(self, didUpdate: cgmManagerStatus) } } public init?(rawState: RawStateValue) { if let mockSensorStateRawValue = rawState["mockSensorState"] as? MockCGMState.RawValue, let mockSensorState = MockCGMState(rawValue: mockSensorStateRawValue) { self.lockedMockSensorState.value = mockSensorState } else { self.lockedMockSensorState.value = MockCGMState(isStateValid: true) } if let dataSourceRawValue = rawState["dataSource"] as? MockCGMDataSource.RawValue, let dataSource = MockCGMDataSource(rawValue: dataSourceRawValue) { self.lockedDataSource.value = dataSource } else { self.lockedDataSource.value = MockCGMDataSource(model: .sineCurve(parameters: (baseGlucose: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 110), amplitude: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 20), period: TimeInterval(hours: 6), referenceDate: Date()))) } setupGlucoseUpdateTimer() } deinit { glucoseUpdateTimer?.invalidate() } public var rawState: RawStateValue { return [ "mockSensorState": mockSensorState.rawValue, "dataSource": dataSource.rawValue ] } public let isOnboarded = true // No distinction between created and onboarded public let appURL: URL? = nil public let providesBLEHeartbeat = false public let managedDataInterval: TimeInterval? = nil public var shouldSyncToRemoteService: Bool { return self.mockSensorState.samplesShouldBeUploaded } public var healthKitStorageDelayEnabled: Bool { get { MockCGMManager.healthKitStorageDelay == fixedHealthKitStorageDelay } set { MockCGMManager.healthKitStorageDelay = newValue ? fixedHealthKitStorageDelay : 0 } } public let fixedHealthKitStorageDelay: TimeInterval = .minutes(2) public static var healthKitStorageDelay: TimeInterval = 0 private func logDeviceComms(_ type: DeviceLogEntryType, message: String) { self.delegate.delegate?.deviceManager(self, logEventForDeviceIdentifier: "mockcgm", type: type, message: message, completion: nil) } private func sendCGMReadingResult(_ result: CGMReadingResult) { if case .newData(let samples) = result, let currentValue = samples.first { mockSensorState.trendType = currentValue.trend mockSensorState.trendRate = currentValue.trendRate mockSensorState.glucoseRangeCategory = glucoseRangeCategory(for: currentValue.quantitySample) issueAlert(for: currentValue) } self.delegate.notify { delegate in delegate?.cgmManager(self, hasNew: result) } } public func glucoseRangeCategory(for glucose: GlucoseSampleValue) -> GlucoseRangeCategory? { switch glucose.quantity { case ...mockSensorState.cgmLowerLimit: return glucose.wasUserEntered ? .urgentLow : .belowRange case mockSensorState.cgmLowerLimit.. Void) { let now = Date() logDeviceComms(.send, message: "Fetch new data") dataSource.fetchNewData { (result) in switch result { case .error(let error): self.logDeviceComms(.error, message: "Error fetching new data: \(error)") case .newData(let samples): self.lastCommunicationDate = now self.logDeviceComms(.receive, message: "New data received: \(samples)") case .unreliableData: self.lastCommunicationDate = now self.logDeviceComms(.receive, message: "Unreliable data received") case .noData: self.lastCommunicationDate = now self.logDeviceComms(.receive, message: "No new data") } completion(result) } } public func backfillData(datingBack duration: TimeInterval) { let now = Date() self.logDeviceComms(.send, message: "backfillData(\(duration))") dataSource.backfillData(from: DateInterval(start: now.addingTimeInterval(-duration), end: now)) { result in switch result { case .error(let error): self.logDeviceComms(.error, message: "Backfill error: \(error)") case .newData(let samples): self.logDeviceComms(.receive, message: "Backfill data: \(samples)") case .unreliableData: self.logDeviceComms(.receive, message: "Backfill data unreliable") case .noData: self.logDeviceComms(.receive, message: "Backfill empty") } self.sendCGMReadingResult(result) } } public func updateGlucoseUpdateTimer() { glucoseUpdateTimer?.invalidate() setupGlucoseUpdateTimer() } private func setupGlucoseUpdateTimer() { glucoseUpdateTimer = Timer.scheduledTimer(withTimeInterval: dataSource.dataPointFrequency.frequency, repeats: true) { [weak self] _ in guard let self = self else { return } self.fetchNewDataIfNeeded() { result in self.sendCGMReadingResult(result) } } } public func injectGlucoseSamples(_ samples: [NewGlucoseSample]) { guard !samples.isEmpty else { return } sendCGMReadingResult(CGMReadingResult.newData(samples.map { NewGlucoseSample($0, device: device) } )) } } fileprivate extension NewGlucoseSample { init(_ other: NewGlucoseSample, device: HKDevice?) { self.init(date: other.date, quantity: other.quantity, condition: other.condition, trend: other.trend, trendRate: other.trendRate, isDisplayOnly: other.isDisplayOnly, wasUserEntered: other.wasUserEntered, syncIdentifier: other.syncIdentifier, syncVersion: other.syncVersion, device: device) } } // MARK: Alert Stuff extension MockCGMManager { public func getSoundBaseURL() -> URL? { return Bundle(for: type(of: self)).bundleURL } public func getSounds() -> [Alert.Sound] { return alerts.map { $1.sound } } public var hasRetractableAlert: Bool { // signal loss alerts can only be removed by switching the CGM data source return currentAlertIdentifier != nil && currentAlertIdentifier != MockCGMManager.signalLoss.identifier } public var currentAlertIdentifier: Alert.AlertIdentifier? { return mockSensorState.cgmStatusHighlight?.alertIdentifier } public func issueAlert(identifier: Alert.AlertIdentifier, trigger: Alert.Trigger, delay: TimeInterval?, metadata: Alert.Metadata? = nil) { guard let alert = alerts[identifier] else { return } delegate.notifyDelayed(by: delay ?? 0) { delegate in self.logDeviceComms(.delegate, message: "\(#function): \(identifier) \(trigger)") delegate?.issueAlert(Alert(identifier: Alert.Identifier(managerIdentifier: self.managerIdentifier, alertIdentifier: identifier), foregroundContent: alert.foregroundContent, backgroundContent: alert.backgroundContent, trigger: trigger, interruptionLevel: alert.interruptionLevel, sound: alert.sound, metadata: metadata)) } // updating the status highlight setStatusHighlight(MockCGMStatusHighlight(localizedMessage: alert.foregroundContent.title, alertIdentifier: alert.identifier)) } public func issueSignalLossAlert() { issueAlert(identifier: MockCGMManager.signalLoss.identifier, trigger: .immediate, delay: nil) } public func retractSignalLossAlert() { retractAlert(identifier: MockCGMManager.signalLoss.identifier) } public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) { self.logDeviceComms(.delegateResponse, message: "\(#function): Alert \(alertIdentifier) acknowledged.") completion(nil) } public func retractCurrentAlert() { guard hasRetractableAlert, let identifier = currentAlertIdentifier else { return } retractAlert(identifier: identifier) } public func retractAlert(identifier: Alert.AlertIdentifier) { delegate.notify { $0?.retractAlert(identifier: Alert.Identifier(managerIdentifier: self.managerIdentifier, alertIdentifier: identifier)) } // updating the status highlight if mockSensorState.cgmStatusHighlight?.alertIdentifier == identifier { setStatusHighlight(nil) } } public func setStatusHighlight(_ statusHighlight: MockCGMStatusHighlight?) { mockSensorState.cgmStatusHighlight = statusHighlight if statusHighlight == nil, case .signalLoss = dataSource.model { // restore signal loss status highlight issueSignalLossAlert() } } private func issueAlert(for glucose: NewGlucoseSample) { guard mockSensorState.glucoseAlertingEnabled else { return } let alertTitle: String let glucoseAlertIdentifier: String let interruptionLevel: Alert.InterruptionLevel switch glucose.quantity { case ...mockSensorState.urgentLowGlucoseThreshold: alertTitle = "Urgent Low Glucose Alert" glucoseAlertIdentifier = "glucose.value.low.urgent" interruptionLevel = .critical case mockSensorState.urgentLowGlucoseThreshold.. 0.5 else { mockSensorState.cgmStatusBadge = MockCGMStatusBadge(badgeType: .lowBattery) return } mockSensorState.cgmStatusBadge = nil } } extension MockCGMManager { public var debugDescription: String { return """ ## MockCGMManager state: \(mockSensorState) dataSource: \(dataSource) """ } } extension MockCGMState: RawRepresentable { public typealias RawValue = [String: Any] public init?(rawValue: RawValue) { guard let isStateValid = rawValue["isStateValid"] as? Bool, let glucoseAlertingEnabled = rawValue["glucoseAlertingEnabled"] as? Bool, let urgentLowGlucoseThresholdValue = rawValue["urgentLowGlucoseThresholdValue"] as? Double, let lowGlucoseThresholdValue = rawValue["lowGlucoseThresholdValue"] as? Double, let highGlucoseThresholdValue = rawValue["highGlucoseThresholdValue"] as? Double, let cgmLowerLimitValue = rawValue["cgmLowerLimitValue"] as? Double, let cgmUpperLimitValue = rawValue["cgmUpperLimitValue"] as? Double else { return nil } self.isStateValid = isStateValid self.glucoseAlertingEnabled = glucoseAlertingEnabled self.samplesShouldBeUploaded = rawValue["samplesShouldBeUploaded"] as? Bool ?? false self.urgentLowGlucoseThresholdValue = urgentLowGlucoseThresholdValue self.lowGlucoseThresholdValue = lowGlucoseThresholdValue self.highGlucoseThresholdValue = highGlucoseThresholdValue self.cgmLowerLimitValue = cgmLowerLimitValue self.cgmUpperLimitValue = cgmUpperLimitValue if let glucoseRangeCategoryRawValue = rawValue["glucoseRangeCategory"] as? GlucoseRangeCategory.RawValue { self.glucoseRangeCategory = GlucoseRangeCategory(rawValue: glucoseRangeCategoryRawValue) } if let localizedMessage = rawValue["localizedMessage"] as? String, let alertIdentifier = rawValue["alertIdentifier"] as? Alert.AlertIdentifier { self.cgmStatusHighlight = MockCGMStatusHighlight(localizedMessage: localizedMessage, alertIdentifier: alertIdentifier) } if let statusBadgeTypeRawValue = rawValue["statusBadgeType"] as? MockCGMStatusBadge.MockCGMStatusBadgeType.RawValue, let statusBadgeType = MockCGMStatusBadge.MockCGMStatusBadgeType(rawValue: statusBadgeTypeRawValue) { self.cgmStatusBadge = MockCGMStatusBadge(badgeType: statusBadgeType) } if let cgmLifecycleProgressRawValue = rawValue["cgmLifecycleProgress"] as? MockCGMLifecycleProgress.RawValue { self.cgmLifecycleProgress = MockCGMLifecycleProgress(rawValue: cgmLifecycleProgressRawValue) } self.progressWarningThresholdPercentValue = rawValue["progressWarningThresholdPercentValue"] as? Double self.progressCriticalThresholdPercentValue = rawValue["progressCriticalThresholdPercentValue"] as? Double self.cgmBatteryChargeRemaining = rawValue["cgmBatteryChargeRemaining"] as? Double setProgressColor() } public var rawValue: RawValue { var rawValue: RawValue = [ "isStateValid": isStateValid, "glucoseAlertingEnabled": glucoseAlertingEnabled, "samplesShouldBeUploaded": samplesShouldBeUploaded, "urgentLowGlucoseThresholdValue": urgentLowGlucoseThresholdValue, "lowGlucoseThresholdValue": lowGlucoseThresholdValue, "highGlucoseThresholdValue": highGlucoseThresholdValue, "cgmLowerLimitValue": cgmLowerLimitValue, "cgmUpperLimitValue": cgmUpperLimitValue, ] if let glucoseRangeCategory = glucoseRangeCategory { rawValue["glucoseRangeCategory"] = glucoseRangeCategory.rawValue } if let cgmStatusHighlight = cgmStatusHighlight { rawValue["localizedMessage"] = cgmStatusHighlight.localizedMessage rawValue["alertIdentifier"] = cgmStatusHighlight.alertIdentifier } if let cgmStatusBadgeType = cgmStatusBadge?.badgeType { rawValue["statusBadgeType"] = cgmStatusBadgeType.rawValue } if let cgmLifecycleProgress = cgmLifecycleProgress { rawValue["cgmLifecycleProgress"] = cgmLifecycleProgress.rawValue } if let progressWarningThresholdPercentValue = progressWarningThresholdPercentValue { rawValue["progressWarningThresholdPercentValue"] = progressWarningThresholdPercentValue } if let progressCriticalThresholdPercentValue = progressCriticalThresholdPercentValue { rawValue["progressCriticalThresholdPercentValue"] = progressCriticalThresholdPercentValue } if let cgmBatteryChargeRemaining = cgmBatteryChargeRemaining { rawValue["cgmBatteryChargeRemaining"] = cgmBatteryChargeRemaining } return rawValue } } extension MockCGMState: CustomDebugStringConvertible { public var debugDescription: String { return """ ## MockCGMState * isStateValid: \(isStateValid) * glucoseAlertingEnabled: \(glucoseAlertingEnabled) * samplesShouldBeUploaded: \(samplesShouldBeUploaded) * urgentLowGlucoseThresholdValue: \(urgentLowGlucoseThresholdValue) * lowGlucoseThresholdValue: \(lowGlucoseThresholdValue) * highGlucoseThresholdValue: \(highGlucoseThresholdValue) * cgmLowerLimitValue: \(cgmLowerLimitValue) * cgmUpperLimitValue: \(cgmUpperLimitValue) * highGlucoseThresholdValue: \(highGlucoseThresholdValue) * glucoseRangeCategory: \(glucoseRangeCategory as Any) * cgmStatusHighlight: \(cgmStatusHighlight as Any) * cgmStatusBadge: \(cgmStatusBadge as Any) * cgmLifecycleProgress: \(cgmLifecycleProgress as Any) * progressWarningThresholdPercentValue: \(progressWarningThresholdPercentValue as Any) * progressCriticalThresholdPercentValue: \(progressCriticalThresholdPercentValue as Any) * cgmBatteryChargeRemaining: \(cgmBatteryChargeRemaining as Any) """ } }