G7SettingsViewModel.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. //
  2. // G7SettingsViewModel.swift
  3. // CGMBLEKitUI
  4. //
  5. // Created by Pete Schwamb on 10/4/22.
  6. // Copyright © 2022 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. import G7SensorKit
  10. import LoopKit
  11. import LoopKitUI
  12. import HealthKit
  13. public enum ColorStyle {
  14. case glucose, warning, critical, normal, dimmed
  15. }
  16. class G7SettingsViewModel: ObservableObject {
  17. @Published private(set) var scanning: Bool = false
  18. @Published private(set) var connected: Bool = false
  19. @Published private(set) var sensorName: String?
  20. @Published private(set) var activatedAt: Date?
  21. @Published private(set) var lastConnect: Date?
  22. @Published private(set) var latestReadingTimestamp: Date?
  23. @Published var uploadReadings: Bool = false {
  24. didSet {
  25. cgmManager.uploadReadings = uploadReadings
  26. }
  27. }
  28. var displayGlucoseUnitObservable: DisplayGlucoseUnitObservable
  29. private var lastReading: G7GlucoseMessage?
  30. lazy var dateFormatter: DateFormatter = {
  31. let formatter = DateFormatter()
  32. formatter.dateStyle = .short
  33. formatter.timeStyle = .short
  34. return formatter
  35. }()
  36. private lazy var glucoseFormatter: QuantityFormatter = {
  37. let formatter = QuantityFormatter()
  38. formatter.setPreferredNumberFormatter(for: displayGlucoseUnitObservable.displayGlucoseUnit)
  39. formatter.numberFormatter.notANumberSymbol = "–"
  40. return formatter
  41. }()
  42. private let quantityFormatter = QuantityFormatter()
  43. private var cgmManager: G7CGMManager
  44. var progressBarState: G7ProgressBarState {
  45. switch cgmManager.lifecycleState {
  46. case .searching:
  47. return .searchingForSensor
  48. case .ok:
  49. return .lifetimeRemaining
  50. case .warmup:
  51. return .warmupProgress
  52. case .failed:
  53. return .sensorFailed
  54. case .gracePeriod:
  55. return .gracePeriodRemaining
  56. case .expired:
  57. return .sensorExpired
  58. }
  59. }
  60. init(cgmManager: G7CGMManager, displayGlucoseUnitObservable: DisplayGlucoseUnitObservable) {
  61. self.cgmManager = cgmManager
  62. self.displayGlucoseUnitObservable = displayGlucoseUnitObservable
  63. updateValues()
  64. self.cgmManager.addStateObserver(self, queue: DispatchQueue.main)
  65. }
  66. func updateValues() {
  67. scanning = cgmManager.isScanning
  68. sensorName = cgmManager.sensorName
  69. activatedAt = cgmManager.sensorActivatedAt
  70. connected = cgmManager.isConnected
  71. lastConnect = cgmManager.lastConnect
  72. lastReading = cgmManager.latestReading
  73. latestReadingTimestamp = cgmManager.latestReadingTimestamp
  74. uploadReadings = cgmManager.state.uploadReadings
  75. }
  76. var progressBarColorStyle: ColorStyle {
  77. switch progressBarState {
  78. case .warmupProgress:
  79. return .glucose
  80. case .searchingForSensor:
  81. return .dimmed
  82. case .sensorExpired, .sensorFailed:
  83. return .critical
  84. case .lifetimeRemaining:
  85. guard let remaining = progressValue else {
  86. return .dimmed
  87. }
  88. if remaining > .hours(24) {
  89. return .glucose
  90. } else {
  91. return .warning
  92. }
  93. case .gracePeriodRemaining:
  94. return .critical
  95. }
  96. }
  97. var progressBarProgress: Double {
  98. switch progressBarState {
  99. case .searchingForSensor:
  100. return 0
  101. case .warmupProgress:
  102. guard let value = progressValue, value > 0 else {
  103. return 0
  104. }
  105. return 1 - value / G7Sensor.warmupDuration
  106. case .lifetimeRemaining:
  107. guard let value = progressValue, value > 0 else {
  108. return 0
  109. }
  110. return 1 - value / G7Sensor.lifetime
  111. case .gracePeriodRemaining:
  112. guard let value = progressValue, value > 0 else {
  113. return 0
  114. }
  115. return 1 - value / G7Sensor.gracePeriod
  116. case .sensorExpired, .sensorFailed:
  117. return 1
  118. }
  119. }
  120. var progressReferenceDate: Date? {
  121. switch progressBarState {
  122. case .searchingForSensor:
  123. return nil
  124. case .sensorExpired, .gracePeriodRemaining:
  125. return cgmManager.sensorEndsAt
  126. case .warmupProgress:
  127. return cgmManager.sensorFinishesWarmupAt
  128. case .lifetimeRemaining:
  129. return cgmManager.sensorExpiresAt
  130. case .sensorFailed:
  131. return nil
  132. }
  133. }
  134. var progressValue: TimeInterval? {
  135. switch progressBarState {
  136. case .sensorExpired, .sensorFailed, .searchingForSensor:
  137. guard let sensorEndsAt = cgmManager.sensorEndsAt else {
  138. return nil
  139. }
  140. return sensorEndsAt.timeIntervalSinceNow
  141. case .warmupProgress:
  142. guard let warmupFinishedAt = cgmManager.sensorFinishesWarmupAt else {
  143. return nil
  144. }
  145. return max(0, warmupFinishedAt.timeIntervalSinceNow)
  146. case .lifetimeRemaining:
  147. guard let expiration = cgmManager.sensorExpiresAt else {
  148. return nil
  149. }
  150. return max(0, expiration.timeIntervalSinceNow)
  151. case .gracePeriodRemaining:
  152. guard let sensorEndsAt = cgmManager.sensorEndsAt else {
  153. return nil
  154. }
  155. return max(0, sensorEndsAt.timeIntervalSinceNow)
  156. }
  157. }
  158. func scanForNewSensor() {
  159. cgmManager.scanForNewSensor()
  160. }
  161. var lastGlucoseString: String {
  162. guard let lastReading = lastReading, lastReading.hasReliableGlucose, let quantity = lastReading.glucoseQuantity else {
  163. return LocalizedString("– – –", comment: "No glucose value representation (3 dashes for mg/dL)")
  164. }
  165. switch lastReading.glucoseRangeCategory {
  166. case .some(.belowRange):
  167. return LocalizedString("LOW", comment: "String displayed instead of a glucose value below the CGM range")
  168. case .some(.aboveRange):
  169. return LocalizedString("HIGH", comment: "String displayed instead of a glucose value above the CGM range")
  170. default:
  171. quantityFormatter.setPreferredNumberFormatter(for: displayGlucoseUnitObservable.displayGlucoseUnit)
  172. let valueStr = quantityFormatter.string(from: quantity, for: displayGlucoseUnitObservable.displayGlucoseUnit, includeUnit: false) ?? ""
  173. return String(format: "%@ %@", valueStr, displayGlucoseUnitObservable.displayGlucoseUnit.shortLocalizedUnitString())
  174. }
  175. }
  176. var lastGlucoseTrendString: String {
  177. if let lastReading = lastReading, lastReading.hasReliableGlucose, let trendRate = lastReading.trendRate {
  178. let glucoseUnitPerMinute = displayGlucoseUnitObservable.displayGlucoseUnit.unitDivided(by: .minute())
  179. // This seemingly strange replacement of glucose units is only to display the unit string correctly
  180. let trendPerMinute = HKQuantity(unit: displayGlucoseUnitObservable.displayGlucoseUnit, doubleValue: trendRate.doubleValue(for: glucoseUnitPerMinute))
  181. let formatted = glucoseFormatter.string(from: trendPerMinute, for: displayGlucoseUnitObservable.displayGlucoseUnit)!
  182. return String(format: LocalizedString("%@/min", comment: "Format string for glucose trend per minute. (1: glucose value and unit)"), formatted)
  183. } else {
  184. return ""
  185. }
  186. }
  187. }
  188. extension G7SettingsViewModel: G7StateObserver {
  189. func g7StateDidUpdate(_ state: G7CGMManagerState?) {
  190. updateValues()
  191. }
  192. func g7ConnectionStatusDidChange() {
  193. updateValues()
  194. }
  195. }