MockCGMManager.swift 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. //
  2. // MockCGMManager.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 LoopKitUI // TODO: DeviceStatusBadge references should live in MockKitUI
  11. import LoopTestingKit
  12. import UIKit
  13. public struct MockCGMState: GlucoseDisplayable {
  14. public var isStateValid: Bool
  15. public var trendType: GlucoseTrend?
  16. public var trendRate: HKQuantity?
  17. public var isLocal: Bool {
  18. return true
  19. }
  20. public var glucoseRangeCategory: GlucoseRangeCategory?
  21. public let unit: HKUnit = .milligramsPerDeciliter
  22. public var glucoseAlertingEnabled: Bool
  23. public var samplesShouldBeUploaded: Bool
  24. private var cgmLowerLimitValue: Double
  25. // HKQuantity isn't codable
  26. public var cgmLowerLimit: HKQuantity {
  27. get {
  28. return HKQuantity.init(unit: unit, doubleValue: cgmLowerLimitValue)
  29. }
  30. set {
  31. var newDoubleValue = newValue.doubleValue(for: unit)
  32. if newDoubleValue >= urgentLowGlucoseThresholdValue {
  33. newDoubleValue = urgentLowGlucoseThresholdValue - 1
  34. }
  35. cgmLowerLimitValue = newDoubleValue
  36. }
  37. }
  38. private var urgentLowGlucoseThresholdValue: Double
  39. public var urgentLowGlucoseThreshold: HKQuantity {
  40. get {
  41. return HKQuantity.init(unit: unit, doubleValue: urgentLowGlucoseThresholdValue)
  42. }
  43. set {
  44. var newDoubleValue = newValue.doubleValue(for: unit)
  45. if newDoubleValue <= cgmLowerLimitValue {
  46. newDoubleValue = cgmLowerLimitValue + 1
  47. }
  48. if newDoubleValue >= lowGlucoseThresholdValue {
  49. newDoubleValue = lowGlucoseThresholdValue - 1
  50. }
  51. urgentLowGlucoseThresholdValue = newDoubleValue
  52. }
  53. }
  54. private var lowGlucoseThresholdValue: Double
  55. public var lowGlucoseThreshold: HKQuantity {
  56. get {
  57. return HKQuantity.init(unit: unit, doubleValue: lowGlucoseThresholdValue)
  58. }
  59. set {
  60. var newDoubleValue = newValue.doubleValue(for: unit)
  61. if newDoubleValue <= urgentLowGlucoseThresholdValue {
  62. newDoubleValue = urgentLowGlucoseThresholdValue + 1
  63. }
  64. if newDoubleValue >= highGlucoseThresholdValue {
  65. newDoubleValue = highGlucoseThresholdValue - 1
  66. }
  67. lowGlucoseThresholdValue = newDoubleValue
  68. }
  69. }
  70. private var highGlucoseThresholdValue: Double
  71. public var highGlucoseThreshold: HKQuantity {
  72. get {
  73. return HKQuantity.init(unit: unit, doubleValue: highGlucoseThresholdValue)
  74. }
  75. set {
  76. var newDoubleValue = newValue.doubleValue(for: unit)
  77. if newDoubleValue <= lowGlucoseThresholdValue {
  78. newDoubleValue = lowGlucoseThresholdValue + 1
  79. }
  80. if newDoubleValue >= cgmUpperLimitValue {
  81. newDoubleValue = cgmUpperLimitValue - 1
  82. }
  83. highGlucoseThresholdValue = newDoubleValue
  84. }
  85. }
  86. private var cgmUpperLimitValue: Double
  87. public var cgmUpperLimit: HKQuantity {
  88. get {
  89. return HKQuantity.init(unit: unit, doubleValue: cgmUpperLimitValue)
  90. }
  91. set {
  92. var newDoubleValue = newValue.doubleValue(for: unit)
  93. if newDoubleValue <= highGlucoseThresholdValue {
  94. newDoubleValue = highGlucoseThresholdValue + 1
  95. }
  96. cgmUpperLimitValue = newDoubleValue
  97. }
  98. }
  99. public var cgmStatusHighlight: MockCGMStatusHighlight?
  100. public var cgmStatusBadge: MockCGMStatusBadge?
  101. public var cgmLifecycleProgress: MockCGMLifecycleProgress? {
  102. didSet {
  103. if cgmLifecycleProgress != oldValue {
  104. setProgressColor()
  105. }
  106. }
  107. }
  108. public var progressWarningThresholdPercentValue: Double? {
  109. didSet {
  110. if progressWarningThresholdPercentValue != oldValue {
  111. setProgressColor()
  112. }
  113. }
  114. }
  115. public var progressCriticalThresholdPercentValue: Double? {
  116. didSet {
  117. if progressCriticalThresholdPercentValue != oldValue {
  118. setProgressColor()
  119. }
  120. }
  121. }
  122. public var cgmBatteryChargeRemaining: Double? = 1
  123. private mutating func setProgressColor() {
  124. guard var cgmLifecycleProgress = cgmLifecycleProgress else {
  125. return
  126. }
  127. if let progressCriticalThresholdPercentValue = progressCriticalThresholdPercentValue,
  128. cgmLifecycleProgress.percentComplete >= progressCriticalThresholdPercentValue
  129. {
  130. cgmLifecycleProgress.progressState = .critical
  131. } else if let progressWarningThresholdPercentValue = progressWarningThresholdPercentValue,
  132. cgmLifecycleProgress.percentComplete >= progressWarningThresholdPercentValue
  133. {
  134. cgmLifecycleProgress.progressState = .warning
  135. } else {
  136. cgmLifecycleProgress.progressState = .normalCGM
  137. }
  138. self.cgmLifecycleProgress = cgmLifecycleProgress
  139. }
  140. public init(isStateValid: Bool = true,
  141. glucoseRangeCategory: GlucoseRangeCategory? = nil,
  142. glucoseAlertingEnabled: Bool = false,
  143. samplesShouldBeUploaded: Bool = false,
  144. urgentLowGlucoseThresholdValue: Double = 55,
  145. lowGlucoseThresholdValue: Double = 80,
  146. highGlucoseThresholdValue: Double = 200,
  147. cgmLowerLimitValue: Double = 40,
  148. cgmUpperLimitValue: Double = 400,
  149. cgmStatusHighlight: MockCGMStatusHighlight? = nil,
  150. cgmLifecycleProgress: MockCGMLifecycleProgress? = nil,
  151. progressWarningThresholdPercentValue: Double? = nil,
  152. progressCriticalThresholdPercentValue: Double? = nil)
  153. {
  154. self.isStateValid = isStateValid
  155. self.glucoseRangeCategory = glucoseRangeCategory
  156. self.glucoseAlertingEnabled = glucoseAlertingEnabled
  157. self.samplesShouldBeUploaded = samplesShouldBeUploaded
  158. self.urgentLowGlucoseThresholdValue = urgentLowGlucoseThresholdValue
  159. self.lowGlucoseThresholdValue = lowGlucoseThresholdValue
  160. self.highGlucoseThresholdValue = highGlucoseThresholdValue
  161. self.cgmLowerLimitValue = cgmLowerLimitValue
  162. self.cgmUpperLimitValue = cgmUpperLimitValue
  163. self.cgmStatusHighlight = cgmStatusHighlight
  164. self.cgmLifecycleProgress = cgmLifecycleProgress
  165. self.progressWarningThresholdPercentValue = progressWarningThresholdPercentValue
  166. self.progressCriticalThresholdPercentValue = progressCriticalThresholdPercentValue
  167. setProgressColor()
  168. }
  169. }
  170. public struct MockCGMStatusHighlight: DeviceStatusHighlight {
  171. public var localizedMessage: String
  172. public var imageName: String {
  173. switch alertIdentifier {
  174. case MockCGMManager.submarine.identifier:
  175. return "dot.radiowaves.left.and.right"
  176. case MockCGMManager.buzz.identifier:
  177. return "clock"
  178. default:
  179. return "exclamationmark.circle.fill"
  180. }
  181. }
  182. public var state: DeviceStatusHighlightState{
  183. switch alertIdentifier {
  184. case MockCGMManager.submarine.identifier:
  185. return .normalCGM
  186. case MockCGMManager.buzz.identifier:
  187. return .warning
  188. default:
  189. return .critical
  190. }
  191. }
  192. public var alertIdentifier: Alert.AlertIdentifier
  193. }
  194. public struct MockCGMStatusBadge: DeviceStatusBadge {
  195. public var image: UIImage? {
  196. return badgeType.image
  197. }
  198. public var state: DeviceStatusBadgeState {
  199. switch badgeType {
  200. case .lowBattery:
  201. return .critical
  202. case .calibrationRequested:
  203. return .warning
  204. }
  205. }
  206. public var badgeType: MockCGMStatusBadgeType
  207. public enum MockCGMStatusBadgeType: Int, CaseIterable {
  208. case lowBattery
  209. case calibrationRequested
  210. var image: UIImage? {
  211. switch self {
  212. case .lowBattery:
  213. return UIImage(frameworkImage: "battery.circle.fill")
  214. case .calibrationRequested:
  215. return UIImage(frameworkImage: "drop.circle.fill")
  216. }
  217. }
  218. }
  219. init(badgeType: MockCGMStatusBadgeType) {
  220. self.badgeType = badgeType
  221. }
  222. }
  223. public struct MockCGMLifecycleProgress: DeviceLifecycleProgress, Equatable {
  224. public var percentComplete: Double
  225. public var progressState: DeviceLifecycleProgressState
  226. public init(percentComplete: Double, progressState: DeviceLifecycleProgressState = .normalCGM) {
  227. self.percentComplete = percentComplete
  228. self.progressState = progressState
  229. }
  230. }
  231. extension MockCGMLifecycleProgress: RawRepresentable {
  232. public typealias RawValue = [String: Any]
  233. public init?(rawValue: RawValue) {
  234. guard let percentComplete = rawValue["percentComplete"] as? Double,
  235. let progressStateRawValue = rawValue["progressState"] as? DeviceLifecycleProgressState.RawValue,
  236. let progressState = DeviceLifecycleProgressState(rawValue: progressStateRawValue) else
  237. {
  238. return nil
  239. }
  240. self.percentComplete = percentComplete
  241. self.progressState = progressState
  242. }
  243. public var rawValue: RawValue {
  244. let rawValue: RawValue = [
  245. "percentComplete": percentComplete,
  246. "progressState": progressState.rawValue,
  247. ]
  248. return rawValue
  249. }
  250. }
  251. public final class MockCGMManager: TestingCGMManager {
  252. public static let managerIdentifier = "MockCGMManager"
  253. public var managerIdentifier: String {
  254. return MockCGMManager.managerIdentifier
  255. }
  256. public static let localizedTitle = "CGM Simulator"
  257. public var localizedTitle: String {
  258. return MockCGMManager.localizedTitle
  259. }
  260. public struct MockAlert {
  261. public let sound: Alert.Sound
  262. public let identifier: Alert.AlertIdentifier
  263. public let foregroundContent: Alert.Content
  264. public let backgroundContent: Alert.Content
  265. public let interruptionLevel: Alert.InterruptionLevel
  266. }
  267. let alerts: [Alert.AlertIdentifier: MockAlert] = [
  268. submarine.identifier: submarine, buzz.identifier: buzz, critical.identifier: critical, signalLoss.identifier: signalLoss
  269. ]
  270. public static let submarine = MockAlert(sound: .sound(name: "sub.caf"), identifier: "submarine",
  271. foregroundContent: Alert.Content(title: "Alert: FG Title", body: "Alert: Foreground Body", acknowledgeActionButtonLabel: "FG OK"),
  272. backgroundContent: Alert.Content(title: "Alert: BG Title", body: "Alert: Background Body", acknowledgeActionButtonLabel: "BG OK"),
  273. interruptionLevel: .timeSensitive)
  274. public static let critical = MockAlert(sound: .sound(name: "critical.caf"), identifier: "critical",
  275. foregroundContent: Alert.Content(title: "Critical Alert: FG Title", body: "Critical Alert: Foreground Body", acknowledgeActionButtonLabel: "Critical FG OK"),
  276. backgroundContent: Alert.Content(title: "Critical Alert: BG Title", body: "Critical Alert: Background Body", acknowledgeActionButtonLabel: "Critical BG OK"),
  277. interruptionLevel: .critical)
  278. public static let buzz = MockAlert(sound: .vibrate, identifier: "buzz",
  279. foregroundContent: Alert.Content(title: "Alert: FG Title", body: "FG bzzzt", acknowledgeActionButtonLabel: "Buzz"),
  280. backgroundContent: Alert.Content(title: "Alert: BG Title", body: "BG bzzzt", acknowledgeActionButtonLabel: "Buzz"),
  281. interruptionLevel: .active)
  282. public static let signalLoss = MockAlert(sound: .sound(name: "critical.caf"),
  283. identifier: "signalLoss",
  284. foregroundContent: Alert.Content(title: "Signal Loss", body: "CGM simulator signal loss", acknowledgeActionButtonLabel: "Dismiss"),
  285. backgroundContent: Alert.Content(title: "Signal Loss", body: "CGM simulator signal loss", acknowledgeActionButtonLabel: "Dismiss"),
  286. interruptionLevel: .critical)
  287. private let lockedMockSensorState = Locked(MockCGMState(isStateValid: true))
  288. public var mockSensorState: MockCGMState {
  289. get {
  290. lockedMockSensorState.value
  291. }
  292. set {
  293. lockedMockSensorState.mutate { $0 = newValue }
  294. self.notifyStatusObservers(cgmManagerStatus: self.cgmManagerStatus)
  295. }
  296. }
  297. public var glucoseDisplay: GlucoseDisplayable? {
  298. return mockSensorState
  299. }
  300. public var cgmManagerStatus: CGMManagerStatus {
  301. return CGMManagerStatus(hasValidSensorSession: dataSource.isValidSession, lastCommunicationDate: lastCommunicationDate, device: device)
  302. }
  303. private var lastCommunicationDate: Date? = nil
  304. public var testingDevice: HKDevice {
  305. return MockCGMDataSource.device
  306. }
  307. public var device: HKDevice? {
  308. return testingDevice
  309. }
  310. public weak var cgmManagerDelegate: CGMManagerDelegate? {
  311. get {
  312. return delegate.delegate
  313. }
  314. set {
  315. delegate.delegate = newValue
  316. }
  317. }
  318. public var delegateQueue: DispatchQueue! {
  319. get {
  320. return delegate.queue
  321. }
  322. set {
  323. delegate.queue = newValue
  324. }
  325. }
  326. private let delegate = WeakSynchronizedDelegate<CGMManagerDelegate>()
  327. private let lockedDataSource = Locked(MockCGMDataSource(model: .noData))
  328. public var dataSource: MockCGMDataSource {
  329. get {
  330. lockedDataSource.value
  331. }
  332. set {
  333. lockedDataSource.mutate { $0 = newValue }
  334. self.notifyStatusObservers(cgmManagerStatus: self.cgmManagerStatus)
  335. }
  336. }
  337. private var glucoseUpdateTimer: Timer?
  338. public init() {
  339. setupGlucoseUpdateTimer()
  340. }
  341. // MARK: Handling CGM Manager Status observers
  342. private var statusObservers = WeakSynchronizedSet<CGMManagerStatusObserver>()
  343. public func addStatusObserver(_ observer: CGMManagerStatusObserver, queue: DispatchQueue) {
  344. statusObservers.insert(observer, queue: queue)
  345. }
  346. public func removeStatusObserver(_ observer: CGMManagerStatusObserver) {
  347. statusObservers.removeElement(observer)
  348. }
  349. private func notifyStatusObservers(cgmManagerStatus: CGMManagerStatus) {
  350. delegate.notify { delegate in
  351. delegate?.cgmManagerDidUpdateState(self)
  352. delegate?.cgmManager(self, didUpdate: self.cgmManagerStatus)
  353. }
  354. statusObservers.forEach { observer in
  355. observer.cgmManager(self, didUpdate: cgmManagerStatus)
  356. }
  357. }
  358. public init?(rawState: RawStateValue) {
  359. if let mockSensorStateRawValue = rawState["mockSensorState"] as? MockCGMState.RawValue,
  360. let mockSensorState = MockCGMState(rawValue: mockSensorStateRawValue) {
  361. self.lockedMockSensorState.value = mockSensorState
  362. } else {
  363. self.lockedMockSensorState.value = MockCGMState(isStateValid: true)
  364. }
  365. if let dataSourceRawValue = rawState["dataSource"] as? MockCGMDataSource.RawValue,
  366. let dataSource = MockCGMDataSource(rawValue: dataSourceRawValue) {
  367. self.lockedDataSource.value = dataSource
  368. } else {
  369. 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())))
  370. }
  371. setupGlucoseUpdateTimer()
  372. }
  373. deinit {
  374. glucoseUpdateTimer?.invalidate()
  375. }
  376. public var rawState: RawStateValue {
  377. return [
  378. "mockSensorState": mockSensorState.rawValue,
  379. "dataSource": dataSource.rawValue
  380. ]
  381. }
  382. public let isOnboarded = true // No distinction between created and onboarded
  383. public let appURL: URL? = nil
  384. public let providesBLEHeartbeat = false
  385. public let managedDataInterval: TimeInterval? = nil
  386. public var shouldSyncToRemoteService: Bool {
  387. return self.mockSensorState.samplesShouldBeUploaded
  388. }
  389. public var healthKitStorageDelayEnabled: Bool {
  390. get {
  391. MockCGMManager.healthKitStorageDelay == fixedHealthKitStorageDelay
  392. }
  393. set {
  394. MockCGMManager.healthKitStorageDelay = newValue ? fixedHealthKitStorageDelay : 0
  395. }
  396. }
  397. public let fixedHealthKitStorageDelay: TimeInterval = .minutes(2)
  398. public static var healthKitStorageDelay: TimeInterval = 0
  399. private func logDeviceComms(_ type: DeviceLogEntryType, message: String) {
  400. self.delegate.delegate?.deviceManager(self, logEventForDeviceIdentifier: "mockcgm", type: type, message: message, completion: nil)
  401. }
  402. private func sendCGMReadingResult(_ result: CGMReadingResult) {
  403. if case .newData(let samples) = result,
  404. let currentValue = samples.first
  405. {
  406. mockSensorState.trendType = currentValue.trend
  407. mockSensorState.trendRate = currentValue.trendRate
  408. mockSensorState.glucoseRangeCategory = glucoseRangeCategory(for: currentValue.quantitySample)
  409. issueAlert(for: currentValue)
  410. }
  411. self.delegate.notify { delegate in
  412. delegate?.cgmManager(self, hasNew: result)
  413. }
  414. }
  415. public func glucoseRangeCategory(for glucose: GlucoseSampleValue) -> GlucoseRangeCategory? {
  416. switch glucose.quantity {
  417. case ...mockSensorState.cgmLowerLimit:
  418. return glucose.wasUserEntered ? .urgentLow : .belowRange
  419. case mockSensorState.cgmLowerLimit..<mockSensorState.urgentLowGlucoseThreshold:
  420. return .urgentLow
  421. case mockSensorState.urgentLowGlucoseThreshold..<mockSensorState.lowGlucoseThreshold:
  422. return .low
  423. case mockSensorState.lowGlucoseThreshold..<mockSensorState.highGlucoseThreshold:
  424. return .normal
  425. case mockSensorState.highGlucoseThreshold..<mockSensorState.cgmUpperLimit:
  426. return .high
  427. default:
  428. return glucose.wasUserEntered ? .high : .aboveRange
  429. }
  430. }
  431. public func fetchNewDataIfNeeded(_ completion: @escaping (CGMReadingResult) -> Void) {
  432. let now = Date()
  433. logDeviceComms(.send, message: "Fetch new data")
  434. dataSource.fetchNewData { (result) in
  435. switch result {
  436. case .error(let error):
  437. self.logDeviceComms(.error, message: "Error fetching new data: \(error)")
  438. case .newData(let samples):
  439. self.lastCommunicationDate = now
  440. self.logDeviceComms(.receive, message: "New data received: \(samples)")
  441. case .unreliableData:
  442. self.lastCommunicationDate = now
  443. self.logDeviceComms(.receive, message: "Unreliable data received")
  444. case .noData:
  445. self.lastCommunicationDate = now
  446. self.logDeviceComms(.receive, message: "No new data")
  447. }
  448. completion(result)
  449. }
  450. }
  451. public func backfillData(datingBack duration: TimeInterval) {
  452. let now = Date()
  453. self.logDeviceComms(.send, message: "backfillData(\(duration))")
  454. dataSource.backfillData(from: DateInterval(start: now.addingTimeInterval(-duration), end: now)) { result in
  455. switch result {
  456. case .error(let error):
  457. self.logDeviceComms(.error, message: "Backfill error: \(error)")
  458. case .newData(let samples):
  459. self.logDeviceComms(.receive, message: "Backfill data: \(samples)")
  460. case .unreliableData:
  461. self.logDeviceComms(.receive, message: "Backfill data unreliable")
  462. case .noData:
  463. self.logDeviceComms(.receive, message: "Backfill empty")
  464. }
  465. self.sendCGMReadingResult(result)
  466. }
  467. }
  468. public func updateGlucoseUpdateTimer() {
  469. glucoseUpdateTimer?.invalidate()
  470. setupGlucoseUpdateTimer()
  471. }
  472. private func setupGlucoseUpdateTimer() {
  473. glucoseUpdateTimer = Timer.scheduledTimer(withTimeInterval: dataSource.dataPointFrequency.frequency, repeats: true) { [weak self] _ in
  474. guard let self = self else { return }
  475. self.fetchNewDataIfNeeded() { result in
  476. self.sendCGMReadingResult(result)
  477. }
  478. }
  479. }
  480. public func injectGlucoseSamples(_ samples: [NewGlucoseSample]) {
  481. guard !samples.isEmpty else { return }
  482. sendCGMReadingResult(CGMReadingResult.newData(samples.map { NewGlucoseSample($0, device: device) } ))
  483. }
  484. }
  485. fileprivate extension NewGlucoseSample {
  486. init(_ other: NewGlucoseSample, device: HKDevice?) {
  487. self.init(date: other.date,
  488. quantity: other.quantity,
  489. condition: other.condition,
  490. trend: other.trend,
  491. trendRate: other.trendRate,
  492. isDisplayOnly: other.isDisplayOnly,
  493. wasUserEntered: other.wasUserEntered,
  494. syncIdentifier: other.syncIdentifier,
  495. syncVersion: other.syncVersion,
  496. device: device)
  497. }
  498. }
  499. // MARK: Alert Stuff
  500. extension MockCGMManager {
  501. public func getSoundBaseURL() -> URL? {
  502. return Bundle(for: type(of: self)).bundleURL
  503. }
  504. public func getSounds() -> [Alert.Sound] {
  505. return alerts.map { $1.sound }
  506. }
  507. public var hasRetractableAlert: Bool {
  508. // signal loss alerts can only be removed by switching the CGM data source
  509. return currentAlertIdentifier != nil && currentAlertIdentifier != MockCGMManager.signalLoss.identifier
  510. }
  511. public var currentAlertIdentifier: Alert.AlertIdentifier? {
  512. return mockSensorState.cgmStatusHighlight?.alertIdentifier
  513. }
  514. public func issueAlert(identifier: Alert.AlertIdentifier, trigger: Alert.Trigger, delay: TimeInterval?, metadata: Alert.Metadata? = nil) {
  515. guard let alert = alerts[identifier] else {
  516. return
  517. }
  518. delegate.notifyDelayed(by: delay ?? 0) { delegate in
  519. self.logDeviceComms(.delegate, message: "\(#function): \(identifier) \(trigger)")
  520. delegate?.issueAlert(Alert(identifier: Alert.Identifier(managerIdentifier: self.managerIdentifier, alertIdentifier: identifier),
  521. foregroundContent: alert.foregroundContent,
  522. backgroundContent: alert.backgroundContent,
  523. trigger: trigger,
  524. interruptionLevel: alert.interruptionLevel,
  525. sound: alert.sound,
  526. metadata: metadata))
  527. }
  528. // updating the status highlight
  529. setStatusHighlight(MockCGMStatusHighlight(localizedMessage: alert.foregroundContent.title, alertIdentifier: alert.identifier))
  530. }
  531. public func issueSignalLossAlert() {
  532. issueAlert(identifier: MockCGMManager.signalLoss.identifier, trigger: .immediate, delay: nil)
  533. }
  534. public func retractSignalLossAlert() {
  535. retractAlert(identifier: MockCGMManager.signalLoss.identifier)
  536. }
  537. public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) {
  538. self.logDeviceComms(.delegateResponse, message: "\(#function): Alert \(alertIdentifier) acknowledged.")
  539. completion(nil)
  540. }
  541. public func retractCurrentAlert() {
  542. guard hasRetractableAlert, let identifier = currentAlertIdentifier else { return }
  543. retractAlert(identifier: identifier)
  544. }
  545. public func retractAlert(identifier: Alert.AlertIdentifier) {
  546. delegate.notify { $0?.retractAlert(identifier: Alert.Identifier(managerIdentifier: self.managerIdentifier, alertIdentifier: identifier)) }
  547. // updating the status highlight
  548. if mockSensorState.cgmStatusHighlight?.alertIdentifier == identifier {
  549. setStatusHighlight(nil)
  550. }
  551. }
  552. public func setStatusHighlight(_ statusHighlight: MockCGMStatusHighlight?) {
  553. mockSensorState.cgmStatusHighlight = statusHighlight
  554. if statusHighlight == nil,
  555. case .signalLoss = dataSource.model
  556. {
  557. // restore signal loss status highlight
  558. issueSignalLossAlert()
  559. }
  560. }
  561. private func issueAlert(for glucose: NewGlucoseSample) {
  562. guard mockSensorState.glucoseAlertingEnabled else {
  563. return
  564. }
  565. let alertTitle: String
  566. let glucoseAlertIdentifier: String
  567. let interruptionLevel: Alert.InterruptionLevel
  568. switch glucose.quantity {
  569. case ...mockSensorState.urgentLowGlucoseThreshold:
  570. alertTitle = "Urgent Low Glucose Alert"
  571. glucoseAlertIdentifier = "glucose.value.low.urgent"
  572. interruptionLevel = .critical
  573. case mockSensorState.urgentLowGlucoseThreshold..<mockSensorState.lowGlucoseThreshold:
  574. alertTitle = "Low Glucose Alert"
  575. glucoseAlertIdentifier = "glucose.value.low"
  576. interruptionLevel = .timeSensitive
  577. case mockSensorState.highGlucoseThreshold...:
  578. alertTitle = "High Glucose Alert"
  579. glucoseAlertIdentifier = "glucose.value.high"
  580. interruptionLevel = .timeSensitive
  581. default:
  582. return
  583. }
  584. let alertIdentifier = Alert.Identifier(managerIdentifier: self.managerIdentifier,
  585. alertIdentifier: glucoseAlertIdentifier)
  586. let alertContent = Alert.Content(title: alertTitle,
  587. body: "The glucose measurement received triggered this alert",
  588. acknowledgeActionButtonLabel: "Dismiss")
  589. let alert = Alert(identifier: alertIdentifier,
  590. foregroundContent: alertContent,
  591. backgroundContent: alertContent,
  592. trigger: .immediate,
  593. interruptionLevel: interruptionLevel)
  594. delegate.notify { delegate in
  595. delegate?.issueAlert(alert)
  596. }
  597. }
  598. }
  599. //MARK: Device Status Badge stuff
  600. extension MockCGMManager {
  601. public func requestCalibration(_ requestCalibration: Bool) {
  602. mockSensorState.cgmStatusBadge = requestCalibration ? MockCGMStatusBadge(badgeType: .calibrationRequested) : nil
  603. checkAndSetBatteryBadge()
  604. }
  605. public var cgmBatteryChargeRemaining: Double? {
  606. get {
  607. return mockSensorState.cgmBatteryChargeRemaining
  608. }
  609. set {
  610. mockSensorState.cgmBatteryChargeRemaining = newValue
  611. checkAndSetBatteryBadge()
  612. }
  613. }
  614. public var isCalibrationRequested: Bool {
  615. return mockSensorState.cgmStatusBadge?.badgeType == .calibrationRequested
  616. }
  617. private func checkAndSetBatteryBadge() {
  618. // calibration badge is the highest priority
  619. guard mockSensorState.cgmStatusBadge?.badgeType != .calibrationRequested else {
  620. return
  621. }
  622. guard let cgmBatteryChargeRemaining = mockSensorState.cgmBatteryChargeRemaining,
  623. cgmBatteryChargeRemaining > 0.5 else
  624. {
  625. mockSensorState.cgmStatusBadge = MockCGMStatusBadge(badgeType: .lowBattery)
  626. return
  627. }
  628. mockSensorState.cgmStatusBadge = nil
  629. }
  630. }
  631. extension MockCGMManager {
  632. public var debugDescription: String {
  633. return """
  634. ## MockCGMManager
  635. state: \(mockSensorState)
  636. dataSource: \(dataSource)
  637. """
  638. }
  639. }
  640. extension MockCGMState: RawRepresentable {
  641. public typealias RawValue = [String: Any]
  642. public init?(rawValue: RawValue) {
  643. guard let isStateValid = rawValue["isStateValid"] as? Bool,
  644. let glucoseAlertingEnabled = rawValue["glucoseAlertingEnabled"] as? Bool,
  645. let urgentLowGlucoseThresholdValue = rawValue["urgentLowGlucoseThresholdValue"] as? Double,
  646. let lowGlucoseThresholdValue = rawValue["lowGlucoseThresholdValue"] as? Double,
  647. let highGlucoseThresholdValue = rawValue["highGlucoseThresholdValue"] as? Double,
  648. let cgmLowerLimitValue = rawValue["cgmLowerLimitValue"] as? Double,
  649. let cgmUpperLimitValue = rawValue["cgmUpperLimitValue"] as? Double else
  650. {
  651. return nil
  652. }
  653. self.isStateValid = isStateValid
  654. self.glucoseAlertingEnabled = glucoseAlertingEnabled
  655. self.samplesShouldBeUploaded = rawValue["samplesShouldBeUploaded"] as? Bool ?? false
  656. self.urgentLowGlucoseThresholdValue = urgentLowGlucoseThresholdValue
  657. self.lowGlucoseThresholdValue = lowGlucoseThresholdValue
  658. self.highGlucoseThresholdValue = highGlucoseThresholdValue
  659. self.cgmLowerLimitValue = cgmLowerLimitValue
  660. self.cgmUpperLimitValue = cgmUpperLimitValue
  661. if let glucoseRangeCategoryRawValue = rawValue["glucoseRangeCategory"] as? GlucoseRangeCategory.RawValue {
  662. self.glucoseRangeCategory = GlucoseRangeCategory(rawValue: glucoseRangeCategoryRawValue)
  663. }
  664. if let localizedMessage = rawValue["localizedMessage"] as? String,
  665. let alertIdentifier = rawValue["alertIdentifier"] as? Alert.AlertIdentifier
  666. {
  667. self.cgmStatusHighlight = MockCGMStatusHighlight(localizedMessage: localizedMessage, alertIdentifier: alertIdentifier)
  668. }
  669. if let statusBadgeTypeRawValue = rawValue["statusBadgeType"] as? MockCGMStatusBadge.MockCGMStatusBadgeType.RawValue,
  670. let statusBadgeType = MockCGMStatusBadge.MockCGMStatusBadgeType(rawValue: statusBadgeTypeRawValue)
  671. {
  672. self.cgmStatusBadge = MockCGMStatusBadge(badgeType: statusBadgeType)
  673. }
  674. if let cgmLifecycleProgressRawValue = rawValue["cgmLifecycleProgress"] as? MockCGMLifecycleProgress.RawValue {
  675. self.cgmLifecycleProgress = MockCGMLifecycleProgress(rawValue: cgmLifecycleProgressRawValue)
  676. }
  677. self.progressWarningThresholdPercentValue = rawValue["progressWarningThresholdPercentValue"] as? Double
  678. self.progressCriticalThresholdPercentValue = rawValue["progressCriticalThresholdPercentValue"] as? Double
  679. self.cgmBatteryChargeRemaining = rawValue["cgmBatteryChargeRemaining"] as? Double
  680. setProgressColor()
  681. }
  682. public var rawValue: RawValue {
  683. var rawValue: RawValue = [
  684. "isStateValid": isStateValid,
  685. "glucoseAlertingEnabled": glucoseAlertingEnabled,
  686. "samplesShouldBeUploaded": samplesShouldBeUploaded,
  687. "urgentLowGlucoseThresholdValue": urgentLowGlucoseThresholdValue,
  688. "lowGlucoseThresholdValue": lowGlucoseThresholdValue,
  689. "highGlucoseThresholdValue": highGlucoseThresholdValue,
  690. "cgmLowerLimitValue": cgmLowerLimitValue,
  691. "cgmUpperLimitValue": cgmUpperLimitValue,
  692. ]
  693. if let glucoseRangeCategory = glucoseRangeCategory {
  694. rawValue["glucoseRangeCategory"] = glucoseRangeCategory.rawValue
  695. }
  696. if let cgmStatusHighlight = cgmStatusHighlight {
  697. rawValue["localizedMessage"] = cgmStatusHighlight.localizedMessage
  698. rawValue["alertIdentifier"] = cgmStatusHighlight.alertIdentifier
  699. }
  700. if let cgmStatusBadgeType = cgmStatusBadge?.badgeType {
  701. rawValue["statusBadgeType"] = cgmStatusBadgeType.rawValue
  702. }
  703. if let cgmLifecycleProgress = cgmLifecycleProgress {
  704. rawValue["cgmLifecycleProgress"] = cgmLifecycleProgress.rawValue
  705. }
  706. if let progressWarningThresholdPercentValue = progressWarningThresholdPercentValue {
  707. rawValue["progressWarningThresholdPercentValue"] = progressWarningThresholdPercentValue
  708. }
  709. if let progressCriticalThresholdPercentValue = progressCriticalThresholdPercentValue {
  710. rawValue["progressCriticalThresholdPercentValue"] = progressCriticalThresholdPercentValue
  711. }
  712. if let cgmBatteryChargeRemaining = cgmBatteryChargeRemaining {
  713. rawValue["cgmBatteryChargeRemaining"] = cgmBatteryChargeRemaining
  714. }
  715. return rawValue
  716. }
  717. }
  718. extension MockCGMState: CustomDebugStringConvertible {
  719. public var debugDescription: String {
  720. return """
  721. ## MockCGMState
  722. * isStateValid: \(isStateValid)
  723. * glucoseAlertingEnabled: \(glucoseAlertingEnabled)
  724. * samplesShouldBeUploaded: \(samplesShouldBeUploaded)
  725. * urgentLowGlucoseThresholdValue: \(urgentLowGlucoseThresholdValue)
  726. * lowGlucoseThresholdValue: \(lowGlucoseThresholdValue)
  727. * highGlucoseThresholdValue: \(highGlucoseThresholdValue)
  728. * cgmLowerLimitValue: \(cgmLowerLimitValue)
  729. * cgmUpperLimitValue: \(cgmUpperLimitValue)
  730. * highGlucoseThresholdValue: \(highGlucoseThresholdValue)
  731. * glucoseRangeCategory: \(glucoseRangeCategory as Any)
  732. * cgmStatusHighlight: \(cgmStatusHighlight as Any)
  733. * cgmStatusBadge: \(cgmStatusBadge as Any)
  734. * cgmLifecycleProgress: \(cgmLifecycleProgress as Any)
  735. * progressWarningThresholdPercentValue: \(progressWarningThresholdPercentValue as Any)
  736. * progressCriticalThresholdPercentValue: \(progressCriticalThresholdPercentValue as Any)
  737. * cgmBatteryChargeRemaining: \(cgmBatteryChargeRemaining as Any)
  738. """
  739. }
  740. }