// LoopFollow // DeviceStatusOpenAPS.swift import Foundation import HealthKit import UIKit extension MainViewController { func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { Storage.shared.device.value = lastDeviceStatus?["device"] as? String ?? "" if lastLoopRecord["failureReason"] != nil { Observable.shared.loopStatusText.value = "X" latestLoopStatusString = "X" } else { guard let enactedOrSuggested = lastLoopRecord["suggested"] as? [String: AnyObject] ?? lastLoopRecord["enacted"] as? [String: AnyObject] else { Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" return } var updatedTime: TimeInterval? if let timestamp = enactedOrSuggested["deliverAt"] as? String ?? enactedOrSuggested["timestamp"] as? String, let parsedTime = formatter.date(from: timestamp)?.timeIntervalSince1970 { updatedTime = parsedTime let formattedTime = Localizer.formatTimestampToLocalString(parsedTime) infoManager.updateInfoData(type: .updated, value: formattedTime) Observable.shared.enactedOrSuggested.value = updatedTime } // ISF let profileISF = profileManager.currentISF() var enactedISF: HKQuantity? // Some uploaders (e.g. Trio Swift oref with dynamic ISF) report a // placeholder 0 in the structured ISF field; treat that as missing. if let enactedISFValue = enactedOrSuggested["ISF"] as? Double, enactedISFValue != 0 { enactedISF = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: enactedISFValue) } if let profileISF = profileISF, let enactedISF = enactedISF, profileISF != enactedISF { infoManager.updateInfoData(type: .isf, firstValue: profileISF, secondValue: enactedISF, separator: .arrow) Storage.shared.lastIsfMgdlPerU.value = enactedISF.doubleValue(for: .milligramsPerDeciliter) } else if let profileISF = profileISF { infoManager.updateInfoData(type: .isf, value: profileISF) Storage.shared.lastIsfMgdlPerU.value = profileISF.doubleValue(for: .milligramsPerDeciliter) } // Carb Ratio (CR) // Prefer the structured CR field; fall back to parsing it out of the // reason string for uploaders that don't expose it (a CR of 0 is never // valid, so it's treated as missing). let profileCR = profileManager.currentCarbRatio() var enactedCR: Double? if let structuredCR = enactedOrSuggested["CR"] as? Double, structuredCR != 0 { enactedCR = structuredCR } else if let reasonString = enactedOrSuggested["reason"] as? String { let pattern = "CR: (\\d+(?:\\.\\d+)?)" if let regex = try? NSRegularExpression(pattern: pattern) { let nsString = reasonString as NSString if let match = regex.firstMatch(in: reasonString, range: NSRange(location: 0, length: nsString.length)) { let crString = nsString.substring(with: match.range(at: 1)) enactedCR = Double(crString) } } } if let profileCR = profileCR, let enactedCR = enactedCR, profileCR != enactedCR { infoManager.updateInfoData(type: .carbRatio, value: profileCR, enactedValue: enactedCR, separator: .arrow) Storage.shared.lastCarbRatio.value = enactedCR } else if let profileCR = profileCR { infoManager.updateInfoData(type: .carbRatio, value: profileCR) Storage.shared.lastCarbRatio.value = profileCR } // IOB if let iobMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") { infoManager.updateInfoData(type: .iob, value: iobMetric) latestIOB = iobMetric Observable.shared.iobText.value = iobMetric.formattedValue() } // COB if let cobMetric = CarbMetric(from: enactedOrSuggested, key: "COB") { infoManager.updateInfoData(type: .cob, value: cobMetric) latestCOB = cobMetric } else if let reasonString = enactedOrSuggested["reason"] as? String { // Fallback: Extract COB from reason string let cobPattern = "COB: (\\d+(?:\\.\\d+)?)" if let cobRegex = try? NSRegularExpression(pattern: cobPattern), let cobMatch = cobRegex.firstMatch(in: reasonString, range: NSRange(location: 0, length: reasonString.utf16.count)) { let cobValueString = (reasonString as NSString).substring(with: cobMatch.range(at: 1)) if let cobValue = Double(cobValueString) { let tempDict: [String: AnyObject] = ["COB": cobValue as AnyObject] if let fallbackCobMetric = CarbMetric(from: tempDict, key: "COB") { infoManager.updateInfoData(type: .cob, value: fallbackCobMetric) latestCOB = fallbackCobMetric } else { print("Failed to create CarbMetric from extracted COB value: \(cobValue)") } } else { print("Invalid COB value extracted from reason string: \(cobValueString)") } } else { print("COB pattern not found in reason string.") } } // Autosens if let sens = enactedOrSuggested["sensitivityRatio"] as? Double { let formattedSens = String(format: "%.0f", sens * 100.0) + "%" infoManager.updateInfoData(type: .autosens, value: formattedSens) Storage.shared.lastAutosens.value = sens } // Recommended Bolus if let rec = lastLoopRecord["recommendedBolus"] as? Double { infoManager.updateInfoData(type: .recBolus, value: InsulinFormatter.shared.string(rec)) Observable.shared.deviceRecBolus.value = rec } else { infoManager.clearInfoData(type: .recBolus) Observable.shared.deviceRecBolus.value = nil } // Eventual BG if let eventualBGValue = enactedOrSuggested["eventualBG"] as? Double { let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue) Observable.shared.predictionText.value = Localizer.formatQuantity(eventualBGQuantity) Storage.shared.projectedBgMgdl.value = eventualBGValue } else { Storage.shared.projectedBgMgdl.value = nil } // Target let profileTargetHigh = profileManager.currentTargetHigh() var enactedTarget: HKQuantity? if let enactedTargetValue = enactedOrSuggested["current_target"] as? Double { var targetUnit = HKUnit.milligramsPerDeciliter if enactedTargetValue < 40 { targetUnit = .millimolesPerLiter } enactedTarget = HKQuantity(unit: targetUnit, doubleValue: enactedTargetValue) } if let profileTargetHigh = profileTargetHigh, let enactedTarget = enactedTarget { let profileTargetHighFormatted = Localizer.formatQuantity(profileTargetHigh) let enactedTargetFormatted = Localizer.formatQuantity(enactedTarget) // Compare formatted values to avoid issues with minor floating-point differences // Profile target could be in another unit than enacted target if profileTargetHighFormatted != enactedTargetFormatted { infoManager.updateInfoData(type: .target, firstValue: profileTargetHigh, secondValue: enactedTarget, separator: .arrow) } else { infoManager.updateInfoData(type: .target, value: profileTargetHigh) } let effectiveMgdl = enactedTarget.doubleValue(for: .milligramsPerDeciliter) Storage.shared.lastTargetLowMgdl.value = effectiveMgdl Storage.shared.lastTargetHighMgdl.value = effectiveMgdl } else if let profileTargetHigh = profileTargetHigh { let profileMgdl = profileTargetHigh.doubleValue(for: .milligramsPerDeciliter) Storage.shared.lastTargetLowMgdl.value = profileMgdl Storage.shared.lastTargetHighMgdl.value = profileMgdl } // TDD if let tddMetric = InsulinMetric(from: enactedOrSuggested, key: "TDD") ?? InsulinMetric(from: lastLoopRecord["enacted"], key: "TDD") { infoManager.updateInfoData(type: .tdd, value: tddMetric) Storage.shared.lastTdd.value = tddMetric.value } let predBGsData: [String: AnyObject]? = { if let enacted = lastLoopRecord["suggested"] as? [String: AnyObject], let predBGs = enacted["predBGs"] as? [String: AnyObject] { return predBGs } else if let suggested = lastLoopRecord["enacted"] as? [String: AnyObject], let predBGs = suggested["predBGs"] as? [String: AnyObject] { return predBGs } return nil }() Observable.shared.predictionColor.value = .gray topPredictionBG = Storage.shared.minBGScale.value if let predbgdata = predBGsData { let toLoad = Int(Storage.shared.predictionToLoad.value * 12) var rawPredBGs = [String: [Double]]() var minPredBG = Double.infinity var maxPredBG = -Double.infinity for type in ["ZT", "IOB", "COB", "UAM"] { if let arr = predbgdata[type] as? [Double], !arr.isEmpty { rawPredBGs[type] = arr for i in 0 ... min(toLoad, arr.count - 1) { minPredBG = min(minPredBG, arr[i]) maxPredBG = max(maxPredBG, arr[i]) } } } openAPSPredBGs = rawPredBGs.isEmpty ? nil : rawPredBGs openAPSPredUpdatedTime = updatedTime if minPredBG != Double.infinity, maxPredBG != -Double.infinity { let value = "\(Localizer.toDisplayUnits(String(minPredBG)))/\(Localizer.toDisplayUnits(String(maxPredBG)))" infoManager.updateInfoData(type: .minMax, value: value) Storage.shared.lastMinBgMgdl.value = minPredBG Storage.shared.lastMaxBgMgdl.value = maxPredBG } else { infoManager.clearInfoData(type: .minMax) } updateOpenAPSPredictionDisplay() } else { openAPSPredBGs = nil openAPSPredUpdatedTime = nil } if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] { if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 { var lastBGTime = updatedTime ?? Date().timeIntervalSince1970 if bgData.count > 0 { lastBGTime = bgData[bgData.count - 1].date } if tempBasalTime > lastBGTime { Observable.shared.loopStatusText.value = "⏀" latestLoopStatusString = "⏀" } else { Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" } } } else { Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" } // Live Activity storage Storage.shared.lastIOB.value = latestIOB?.value Storage.shared.lastCOB.value = latestCOB?.value } } }