LoopStatusView.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import SwiftUI
  2. struct LoopStatusView: View {
  3. @Environment(\.colorScheme) var colorScheme
  4. @Environment(AppState.self) var appState
  5. var state: Home.StateModel
  6. @State var sheetDetent = PresentationDetent.medium
  7. @State private var statusTitle: String = ""
  8. var body: some View {
  9. NavigationStack {
  10. VStack(alignment: .leading, spacing: 10) {
  11. // HStack {
  12. Text("Current Loop Status").bold().padding(.top, 20)
  13. Text(statusTitle)
  14. .font(.headline)
  15. .foregroundColor(statusBadgeTextColor)
  16. .padding(.horizontal, 12)
  17. .padding(.vertical, 6)
  18. .background(statusBadgeColor)
  19. .clipShape(Capsule())
  20. // Spacer()
  21. // }
  22. if let errorMessage = state.errorMessage, let date = state.errorDate {
  23. Group {
  24. Text("Error During Algorithm Run at \(Formatter.dateFormatter.string(from: date))").font(.headline)
  25. Text(errorMessage).font(.caption)
  26. }.foregroundColor(.loopRed)
  27. }
  28. if let determination = state.determinationsFromPersistence.first {
  29. if determination.glucose == 400 {
  30. Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed)
  31. .padding(.top, 8)
  32. Text("SMBs and High Temps Disabled.").font(.caption).padding(.bottom, 4)
  33. } else {
  34. Text("Latest Raw Algorithm Output").bold().padding(.top)
  35. Text(
  36. "Trio is currently using these metrics and values as determined by the oref algorithm:"
  37. )
  38. .font(.subheadline)
  39. let tags = !state.isSmoothingEnabled ? determination.reasonParts : determination
  40. .reasonParts + ["Smoothing: On"]
  41. TagCloudView(
  42. tags: tags,
  43. shouldParseToMmolL: state.units == .mmolL
  44. )
  45. .animation(.none, value: false)
  46. Text("Current Algorithm Reasoning").bold().padding(.top)
  47. Text(
  48. self
  49. .parseReasonConclusion(
  50. determination.reasonConclusion,
  51. isMmolL: state.units == .mmolL
  52. )
  53. ).font(.subheadline)
  54. }
  55. } else {
  56. Text("No recent oref algorithm determination.")
  57. }
  58. Spacer()
  59. Button {
  60. state.isLoopStatusPresented.toggle()
  61. } label: {
  62. Text("Got it!")
  63. .frame(maxWidth: .infinity, alignment: .center)
  64. }
  65. .buttonStyle(.bordered)
  66. .padding(.top)
  67. }
  68. .padding(.vertical)
  69. .padding(.horizontal, 20)
  70. .presentationDetents(
  71. [.fraction(0.75), .large],
  72. selection: $sheetDetent
  73. )
  74. .ignoresSafeArea(edges: .top)
  75. .background(appState.trioBackgroundColor(for: colorScheme))
  76. .onAppear {
  77. setStatusTitle()
  78. }
  79. }
  80. .scrollContentBackground(.hidden)
  81. }
  82. private var statusBadgeColor: Color {
  83. guard let determination = state.determinationsFromPersistence.first, determination.timestamp != nil
  84. else {
  85. // previously the .timestamp property was used here because this only gets updated when the reportenacted function in the aps manager gets called
  86. return .secondary
  87. }
  88. let delta = state.timerDate.timeIntervalSince(state.lastLoopDate) - 30
  89. if delta <= 5.minutes.timeInterval {
  90. guard determination.timestamp != nil else {
  91. return .loopYellow
  92. }
  93. return .loopGreen
  94. } else if delta <= 10.minutes.timeInterval {
  95. return .loopYellow
  96. } else {
  97. return .loopRed
  98. }
  99. }
  100. private var statusBadgeTextColor: Color {
  101. statusBadgeColor == .secondary || statusBadgeColor == .loopYellow ? .black :
  102. .white
  103. }
  104. private func setStatusTitle() {
  105. if let determination = state.determinationsFromPersistence.first {
  106. statusTitle =
  107. "Enacted at \(Formatter.dateFormatter.string(from: determination.deliverAt ?? Date()))"
  108. } else {
  109. statusTitle = "Not enacted."
  110. }
  111. }
  112. // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
  113. private func parseReasonConclusion(_ reasonConclusion: String, isMmolL _: Bool) -> String {
  114. let patterns = [
  115. "minGuardBG\\s*-?\\d+\\.?\\d*<-?\\d+\\.?\\d*",
  116. "Eventual BG\\s*-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*",
  117. "\\S+\\s+-?\\d+\\.?\\d*\\s*>\\s*\\d+%\\s+of\\s+BG\\s+-?\\d+\\.?\\d*"
  118. ]
  119. let pattern = patterns.joined(separator: "|")
  120. let regex = try! NSRegularExpression(pattern: pattern)
  121. func convertToMmolL(_ value: String) -> String {
  122. if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
  123. let mmolValue = Decimal(glucoseValue).asMmolL
  124. return mmolValue.description
  125. }
  126. return value
  127. }
  128. let matches = regex.matches(
  129. in: reasonConclusion,
  130. range: NSRange(reasonConclusion.startIndex..., in: reasonConclusion)
  131. )
  132. var updatedConclusion = reasonConclusion
  133. for match in matches.reversed() {
  134. guard let range = Range(match.range, in: reasonConclusion) else { continue }
  135. let matchedString = String(reasonConclusion[range])
  136. if matchedString.contains("<") {
  137. // Handle "minGuardBG x<y" pattern
  138. let parts = matchedString.components(separatedBy: "<")
  139. if parts.count == 2,
  140. let firstValue = Double(
  141. parts[0]
  142. .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
  143. ),
  144. let secondValue = Double(
  145. parts[1]
  146. .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
  147. )
  148. {
  149. let formattedFirstValue = convertToMmolL(String(firstValue))
  150. let formattedSecondValue = convertToMmolL(String(secondValue))
  151. let formattedString = "minGuardBG \(formattedFirstValue)<\(formattedSecondValue)"
  152. updatedConclusion.replaceSubrange(range, with: formattedString)
  153. }
  154. } else if matchedString.contains(">=") {
  155. // Handle "Eventual BG x >= target" pattern
  156. let parts = matchedString.components(separatedBy: " >= ")
  157. if parts.count == 2,
  158. let firstValue = Double(
  159. parts[0]
  160. .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
  161. ),
  162. let secondValue = Double(
  163. parts[1]
  164. .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
  165. )
  166. {
  167. let formattedFirstValue = convertToMmolL(String(firstValue))
  168. let formattedSecondValue = convertToMmolL(String(secondValue))
  169. let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)"
  170. updatedConclusion.replaceSubrange(range, with: formattedString)
  171. }
  172. } else if matchedString.contains(">") {
  173. // Handle "maxDelta 37 > 20% of BG 95" style
  174. let pattern = "(\\S+)\\s+(-?\\d+\\.?\\d*)\\s*>\\s*(\\d+)%\\s+of\\s+BG\\s+(-?\\d+\\.?\\d*)"
  175. let localRegex = try! NSRegularExpression(pattern: pattern)
  176. if let localMatch = localRegex.firstMatch(
  177. in: matchedString,
  178. range: NSRange(matchedString.startIndex..., in: matchedString)
  179. ) {
  180. let metric = String(matchedString[Range(localMatch.range(at: 1), in: matchedString)!])
  181. let firstValue = String(matchedString[Range(localMatch.range(at: 2), in: matchedString)!])
  182. let percentage = String(matchedString[Range(localMatch.range(at: 3), in: matchedString)!])
  183. let bgValue = String(matchedString[Range(localMatch.range(at: 4), in: matchedString)!])
  184. let formattedFirstValue = convertToMmolL(firstValue)
  185. let formattedBGValue = convertToMmolL(bgValue)
  186. let formattedString = "\(metric) \(formattedFirstValue) > \(percentage)% of BG \(formattedBGValue)"
  187. updatedConclusion.replaceSubrange(range, with: formattedString)
  188. }
  189. }
  190. }
  191. return updatedConclusion.capitalizingFirstLetter()
  192. }
  193. }