| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- // LoopFollow
- // DeviceStatusOpenAPS.swift
- import Foundation
- import HealthKit
- 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
- }
- }
- }
|