| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- import SwiftUI
- struct LoopStatusSheetView: View {
- @Environment(\.colorScheme) var colorScheme
- @Environment(AppState.self) var appState
- var state: Home.StateModel
- @State var sheetDetent = PresentationDetent.medium
- @State private var statusTitle: String = ""
- var body: some View {
- NavigationStack {
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- Spacer()
- Text(statusTitle)
- .font(.headline)
- .foregroundColor(statusBadgeTextColor)
- .padding(.horizontal, 12)
- .padding(.vertical, 6)
- .background(statusBadgeColor)
- .clipShape(Capsule())
- Spacer()
- }
- .padding(.top, 35)
- .padding(.bottom)
- if let errorMessage = state.errorMessage, let date = state.errorDate {
- Group {
- Text("Error During Algorithm Run at \(Formatter.dateFormatter.string(from: date))").font(.headline)
- Text(errorMessage).font(.caption)
- }.foregroundColor(.loopRed)
- }
- if let determination = state.determinationsFromPersistence.first {
- if determination.glucose == 400 {
- Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed)
- .padding(.top, 8)
- Text("SMBs and High Temps Disabled.").font(.caption).padding(.bottom, 4)
- } else {
- Text("Latest Raw Algorithm Output").bold()
- Text(
- "Trio is currently using these metrics and values as determined by the oref algorithm:"
- )
- .font(.subheadline)
- let tags = !state.isSmoothingEnabled ? determination.reasonParts : determination
- .reasonParts + ["Smoothing: On"]
- TagCloudView(
- tags: tags,
- shouldParseToMmolL: state.units == .mmolL
- )
- .animation(.none, value: false)
- Divider().padding(.vertical)
- Text("Current Algorithm Reasoning:").bold()
- Text(
- self
- .parseReasonConclusion(
- determination.reasonConclusion,
- isMmolL: state.units == .mmolL
- )
- ).font(.subheadline)
- }
- } else {
- Text("No recent oref algorithm determination.")
- }
- Spacer()
- Button {
- state.isLoopStatusPresented.toggle()
- } label: {
- Text("Got it!")
- .frame(maxWidth: .infinity, alignment: .center)
- }
- .buttonStyle(.bordered)
- .padding(.top)
- }
- .padding(.vertical)
- .padding(.horizontal, 20)
- .presentationDetents(
- [.fraction(0.75), .large],
- selection: $sheetDetent
- )
- .ignoresSafeArea(edges: .top)
- .background(appState.trioBackgroundColor(for: colorScheme))
- .navigationBarTitle("Current Loop Status", displayMode: .inline)
- .onAppear {
- setStatusTitle()
- }
- }
- .scrollContentBackground(.hidden)
- }
- private var statusBadgeColor: Color {
- guard let determination = state.determinationsFromPersistence.first, determination.timestamp != nil
- else {
- // previously the .timestamp property was used here because this only gets updated when the reportenacted function in the aps manager gets called
- return .secondary
- }
- let delta = state.timerDate.timeIntervalSince(state.lastLoopDate) - 30
- if delta <= 5.minutes.timeInterval {
- guard determination.timestamp != nil else {
- return .loopYellow
- }
- return .loopGreen
- } else if delta <= 10.minutes.timeInterval {
- return .loopYellow
- } else {
- return .loopRed
- }
- }
- private var statusBadgeTextColor: Color {
- statusBadgeColor == .secondary || statusBadgeColor == .loopYellow ? .black :
- .white
- }
- private func setStatusTitle() {
- if let determination = state.determinationsFromPersistence.first {
- statusTitle =
- "Enacted at \(Formatter.dateFormatter.string(from: determination.deliverAt ?? Date()))"
- } else {
- statusTitle = "Not enacted."
- }
- }
- // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
- private func parseReasonConclusion(_ reasonConclusion: String, isMmolL _: Bool) -> String {
- let patterns = [
- "minGuardBG\\s*-?\\d+\\.?\\d*<-?\\d+\\.?\\d*",
- "Eventual BG\\s*-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*",
- "\\S+\\s+-?\\d+\\.?\\d*\\s*>\\s*\\d+%\\s+of\\s+BG\\s+-?\\d+\\.?\\d*"
- ]
- let pattern = patterns.joined(separator: "|")
- let regex = try! NSRegularExpression(pattern: pattern)
- func convertToMmolL(_ value: String) -> String {
- if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
- let mmolValue = Decimal(glucoseValue).asMmolL
- return mmolValue.description
- }
- return value
- }
- let matches = regex.matches(
- in: reasonConclusion,
- range: NSRange(reasonConclusion.startIndex..., in: reasonConclusion)
- )
- var updatedConclusion = reasonConclusion
- for match in matches.reversed() {
- guard let range = Range(match.range, in: reasonConclusion) else { continue }
- let matchedString = String(reasonConclusion[range])
- if matchedString.contains("<") {
- // Handle "minGuardBG x<y" pattern
- let parts = matchedString.components(separatedBy: "<")
- if parts.count == 2,
- let firstValue = Double(
- parts[0]
- .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
- ),
- let secondValue = Double(
- parts[1]
- .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
- )
- {
- let formattedFirstValue = convertToMmolL(String(firstValue))
- let formattedSecondValue = convertToMmolL(String(secondValue))
- let formattedString = "minGuardBG \(formattedFirstValue)<\(formattedSecondValue)"
- updatedConclusion.replaceSubrange(range, with: formattedString)
- }
- } else if matchedString.contains(">=") {
- // Handle "Eventual BG x >= target" pattern
- let parts = matchedString.components(separatedBy: " >= ")
- if parts.count == 2,
- let firstValue = Double(
- parts[0]
- .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
- ),
- let secondValue = Double(
- parts[1]
- .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined()
- )
- {
- let formattedFirstValue = convertToMmolL(String(firstValue))
- let formattedSecondValue = convertToMmolL(String(secondValue))
- let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)"
- updatedConclusion.replaceSubrange(range, with: formattedString)
- }
- } else if matchedString.contains(">") {
- // Handle "maxDelta 37 > 20% of BG 95" style
- let pattern = "(\\S+)\\s+(-?\\d+\\.?\\d*)\\s*>\\s*(\\d+)%\\s+of\\s+BG\\s+(-?\\d+\\.?\\d*)"
- let localRegex = try! NSRegularExpression(pattern: pattern)
- if let localMatch = localRegex.firstMatch(
- in: matchedString,
- range: NSRange(matchedString.startIndex..., in: matchedString)
- ) {
- let metric = String(matchedString[Range(localMatch.range(at: 1), in: matchedString)!])
- let firstValue = String(matchedString[Range(localMatch.range(at: 2), in: matchedString)!])
- let percentage = String(matchedString[Range(localMatch.range(at: 3), in: matchedString)!])
- let bgValue = String(matchedString[Range(localMatch.range(at: 4), in: matchedString)!])
- let formattedFirstValue = convertToMmolL(firstValue)
- let formattedBGValue = convertToMmolL(bgValue)
- let formattedString = "\(metric) \(formattedFirstValue) > \(percentage)% of BG \(formattedBGValue)"
- updatedConclusion.replaceSubrange(range, with: formattedString)
- }
- }
- }
- return updatedConclusion.capitalizingFirstLetter()
- }
- }
|