From f89a5f54e455a7fa97258a9a124b547fe5a85716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 28 Jan 2025 18:39:10 +0100 Subject: [PATCH 1/4] Enacted/Suggested for Trio --- .../Controllers/Nightscout/DeviceStatus.swift | 10 +- .../Nightscout/DeviceStatusOpenAPS.swift | 396 ++++++++---------- LoopFollow/Storage/Observable.swift | 1 - 3 files changed, 185 insertions(+), 222 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 45594967..4b99dc02 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -11,14 +11,8 @@ import UIKit import Charts extension MainViewController { - // NS Device Status Web Call func webLoadNSDeviceStatus() { - let count = ObservableUserDefaults.shared.device.value == "Trio" && Observable.shared.isLastDeviceStatusSuggested.value ? "5" : "1" - if count != "1" { - LogManager.shared.log(category: .deviceStatus, message: "Fetching \(count) device status records") - } - - let parameters: [String: String] = ["count": count] + let parameters: [String: String] = ["count": "1"] NightscoutUtils.executeDynamicRequest(eventType: .deviceStatus, parameters: parameters) { result in switch result { case .success(let json): @@ -146,7 +140,7 @@ extension MainViewController { // OpenAPS - handle new data if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String : AnyObject]? { - DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord, jsonDeviceStatus: jsonDeviceStatus) + DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord) } // Start the timer based on the timestamp diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index 35ba3f61..bb682b18 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -8,259 +8,229 @@ import UIKit import HealthKit extension MainViewController { - func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject], jsonDeviceStatus: [[String:AnyObject]]) { - if let createdAtString = lastDeviceStatus?["created_at"] as? String, - let lastLoopTime = formatter.date(from: createdAtString)?.timeIntervalSince1970 { - ObservableUserDefaults.shared.device.value = lastDeviceStatus?["device"] as? String ?? "" - if lastLoopRecord["failureReason"] != nil { - LoopStatusLabel.text = "X" - latestLoopStatusString = "X" + func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { + ObservableUserDefaults.shared.device.value = lastDeviceStatus?["device"] as? String ?? "" + if lastLoopRecord["failureReason"] != nil { + LoopStatusLabel.text = "X" + latestLoopStatusString = "X" + evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) + } else { + guard let enactedOrSuggested = lastLoopRecord["suggested"] as? [String: AnyObject] ?? lastLoopRecord["enacted"] as? [String: AnyObject] else { + LoopStatusLabel.text = "↻" + latestLoopStatusString = "↻" evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) - } else { - guard let enactedOrSuggested = lastLoopRecord["enacted"] as? [String: AnyObject] ?? lastLoopRecord["suggested"] as? [String: AnyObject] else { - LoopStatusLabel.text = "↻" - latestLoopStatusString = "↻" - evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) - return - } - - var wasEnacted : Bool - if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] { - wasEnacted = NSDictionary(dictionary: enacted).isEqual(to: enactedOrSuggested) - } else { - wasEnacted = false - } - - Observable.shared.isLastDeviceStatusSuggested.value = !wasEnacted + return + } - if wasEnacted { + var wasEnacted : Bool + if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] { + wasEnacted = true + if let timestampString = lastDeviceStatus?["timestamp"] as? String, + let lastLoopTime = formatter.date(from: timestampString)?.timeIntervalSince1970 { UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime LogManager.shared.log(category: .deviceStatus, message: "New LastLoopTime: \(lastLoopTime)", isDebug: true) - - evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) - } else { - LogManager.shared.log(category: .deviceStatus, message: "Last devicestatus was not enacted") - findFallbackEnactedAndSetLoopTime(in: jsonDeviceStatus, formatter: formatter) } + } else { + wasEnacted = false + LogManager.shared.log(category: .deviceStatus, message: "Last devicestatus is missing enacted") + } + evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) - if let timestamp = enactedOrSuggested["timestamp"] as? String, - let enactedTime = formatter.date(from: timestamp)?.timeIntervalSince1970 { - let formattedTime = Localizer.formatTimestampToLocalString(enactedTime) - infoManager.updateInfoData(type: .updated, value: formattedTime) - } - // ISF - let profileISF = profileManager.currentISF() - var enactedISF: HKQuantity? - if let enactedISFValue = enactedOrSuggested["ISF"] as? Double { - var determinedISFUnit: HKUnit = .milligramsPerDeciliter - if enactedISFValue < 15 { - determinedISFUnit = .millimolesPerLiter - } - enactedISF = HKQuantity(unit: determinedISFUnit, doubleValue: enactedISFValue) - } - if let profileISF = profileISF, let enactedISF = enactedISF, profileISF != enactedISF { - infoManager.updateInfoData(type: .isf, firstValue: profileISF, secondValue: enactedISF, separator: .arrow) - } else if let profileISF = profileISF { - infoManager.updateInfoData(type: .isf, value: profileISF) + var updatedTime: TimeInterval? + + if let timestamp = enactedOrSuggested["timestamp"] as? String, + let parsedTime = formatter.date(from: timestamp)?.timeIntervalSince1970 { + updatedTime = parsedTime + let formattedTime = Localizer.formatTimestampToLocalString(parsedTime) + infoManager.updateInfoData(type: .updated, value: formattedTime) + } + + // ISF + let profileISF = profileManager.currentISF() + var enactedISF: HKQuantity? + if let enactedISFValue = enactedOrSuggested["ISF"] as? Double { + var determinedISFUnit: HKUnit = .milligramsPerDeciliter + if enactedISFValue < 15 { + determinedISFUnit = .millimolesPerLiter } + enactedISF = HKQuantity(unit: determinedISFUnit, doubleValue: enactedISFValue) + } + if let profileISF = profileISF, let enactedISF = enactedISF, profileISF != enactedISF { + infoManager.updateInfoData(type: .isf, firstValue: profileISF, secondValue: enactedISF, separator: .arrow) + } else if let profileISF = profileISF { + infoManager.updateInfoData(type: .isf, value: profileISF) + } - // Carb Ratio (CR) - let profileCR = profileManager.currentCarbRatio() - var enactedCR: Double? - 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) - } + // Carb Ratio (CR) + let profileCR = profileManager.currentCarbRatio() + var enactedCR: Double? + 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) - } else if let profileCR = profileCR { - infoManager.updateInfoData(type: .carbRatio, value: profileCR) - } + if let profileCR = profileCR, let enactedCR = enactedCR, profileCR != enactedCR { + infoManager.updateInfoData(type: .carbRatio, value: profileCR, enactedValue: enactedCR, separator: .arrow) + } else if let profileCR = profileCR { + infoManager.updateInfoData(type: .carbRatio, value: profileCR) + } - // IOB - if let iobMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") { - infoManager.updateInfoData(type: .iob, value: iobMetric) - latestIOB = iobMetric - } + // IOB + if let iobMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") { + infoManager.updateInfoData(type: .iob, value: iobMetric) + latestIOB = iobMetric + } - // 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)") - } + // 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("Invalid COB value extracted from reason string: \(cobValueString)") + print("Failed to create CarbMetric from extracted COB value: \(cobValue)") } } else { - print("COB pattern not found in reason string.") + print("Invalid COB value extracted from reason string: \(cobValueString)") } - } - - // Insulin Required - if let insulinReqMetric = InsulinMetric(from: enactedOrSuggested, key: "insulinReq") { - infoManager.updateInfoData(type: .recBolus, value: insulinReqMetric) - UserDefaultsRepository.deviceRecBolus.value = insulinReqMetric.value } else { - UserDefaultsRepository.deviceRecBolus.value = 0 - } - - // Autosens - if let sens = enactedOrSuggested["sensitivityRatio"] as? Double { - let formattedSens = String(format: "%.0f", sens * 100.0) + "%" - infoManager.updateInfoData(type: .autosens, value: formattedSens) - } - - // Eventual BG - if let eventualBGValue = enactedOrSuggested["eventualBG"] as? Double { - let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue) - PredictionLabel.text = Localizer.formatQuantity(eventualBGQuantity) + print("COB pattern not found in reason string.") } + } - // 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) - } + // Insulin Required + if let insulinReqMetric = InsulinMetric(from: enactedOrSuggested, key: "insulinReq") { + infoManager.updateInfoData(type: .recBolus, value: insulinReqMetric) + UserDefaultsRepository.deviceRecBolus.value = insulinReqMetric.value + } else { + UserDefaultsRepository.deviceRecBolus.value = 0 + } - if let profileTargetHigh = profileTargetHigh, let enactedTarget = enactedTarget { - let profileTargetHighFormatted = Localizer.formatQuantity(profileTargetHigh) - let enactedTargetFormatted = Localizer.formatQuantity(enactedTarget) + // Autosens + if let sens = enactedOrSuggested["sensitivityRatio"] as? Double { + let formattedSens = String(format: "%.0f", sens * 100.0) + "%" + infoManager.updateInfoData(type: .autosens, value: formattedSens) + } - // 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) - } - } + // Eventual BG + if let eventualBGValue = enactedOrSuggested["eventualBG"] as? Double { + let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue) + PredictionLabel.text = Localizer.formatQuantity(eventualBGQuantity) + } - // TDD - if let tddMetric = InsulinMetric(from: enactedOrSuggested, key: "TDD") { - infoManager.updateInfoData(type: .tdd, value: tddMetric) + // 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) + } - let predictioncolor = UIColor.systemGray - PredictionLabel.textColor = predictioncolor - topPredictionBG = UserDefaultsRepository.minBGScale.value - if let predbgdata = enactedOrSuggested["predBGs"] as? [String: AnyObject] { - let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [ - ("ZT", "ZT", 12), - ("IOB", "Insulin", 13), - ("COB", "LoopYellow", 14), - ("UAM", "UAM", 15) - ] - - var minPredBG = Double.infinity - var maxPredBG = -Double.infinity + if let profileTargetHigh = profileTargetHigh, let enactedTarget = enactedTarget { + let profileTargetHighFormatted = Localizer.formatQuantity(profileTargetHigh) + let enactedTargetFormatted = Localizer.formatQuantity(enactedTarget) - for (type, colorName, dataIndex) in predictionTypes { - var predictionData = [ShareGlucoseData]() - if let graphdata = predbgdata[type] as? [Double] { - var predictionTime = lastLoopTime - let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) + // 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) + } + } - for i in 0...toLoad { - if i < graphdata.count { - let predictionValue = graphdata[i] - minPredBG = min(minPredBG, predictionValue) - maxPredBG = max(maxPredBG, predictionValue) + // TDD + if let tddMetric = InsulinMetric(from: enactedOrSuggested, key: "TDD") { + infoManager.updateInfoData(type: .tdd, value: tddMetric) + } - let prediction = ShareGlucoseData(sgv: Int(round(predictionValue)), date: predictionTime, direction: "flat") - predictionData.append(prediction) - predictionTime += 300 - } + let predictioncolor = UIColor.systemGray + PredictionLabel.textColor = predictioncolor + topPredictionBG = UserDefaultsRepository.minBGScale.value + if let predbgdata = enactedOrSuggested["predBGs"] as? [String: AnyObject] { + let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [ + ("ZT", "ZT", 12), + ("IOB", "Insulin", 13), + ("COB", "LoopYellow", 14), + ("UAM", "UAM", 15) + ] + + var minPredBG = Double.infinity + var maxPredBG = -Double.infinity + + for (type, colorName, dataIndex) in predictionTypes { + var predictionData = [ShareGlucoseData]() + if let graphdata = predbgdata[type] as? [Double] { + var predictionTime = updatedTime ?? Date().timeIntervalSince1970 + let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) + + for i in 0...toLoad { + if i < graphdata.count { + let predictionValue = graphdata[i] + minPredBG = min(minPredBG, predictionValue) + maxPredBG = max(maxPredBG, predictionValue) + + let prediction = ShareGlucoseData(sgv: Int(round(predictionValue)), date: predictionTime, direction: "flat") + predictionData.append(prediction) + predictionTime += 300 } } - - let color = UIColor(named: colorName) ?? UIColor.systemPurple - updatePredictionGraphGeneric( - dataIndex: dataIndex, - predictionData: predictionData, - chartLabel: type, - color: color - ) } - if minPredBG != Double.infinity && maxPredBG != -Double.infinity { - let value = "\(Localizer.toDisplayUnits(String(minPredBG)))/\(Localizer.toDisplayUnits(String(maxPredBG)))" - infoManager.updateInfoData(type: .minMax, value: value) - } else { - infoManager.updateInfoData(type: .minMax, value: "N/A") - } + let color = UIColor(named: colorName) ?? UIColor.systemPurple + updatePredictionGraphGeneric( + dataIndex: dataIndex, + predictionData: predictionData, + chartLabel: type, + color: color + ) } - if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] { - if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 { - var lastBGTime = lastLoopTime - if bgData.count > 0 { - lastBGTime = bgData[bgData.count - 1].date - } - if tempBasalTime > lastBGTime && !wasEnacted { - LoopStatusLabel.text = "⏀" - latestLoopStatusString = "⏀" - } else { - LoopStatusLabel.text = "↻" - latestLoopStatusString = "↻" - } - } + if minPredBG != Double.infinity && maxPredBG != -Double.infinity { + let value = "\(Localizer.toDisplayUnits(String(minPredBG)))/\(Localizer.toDisplayUnits(String(maxPredBG)))" + infoManager.updateInfoData(type: .minMax, value: value) } else { - LoopStatusLabel.text = "↻" - latestLoopStatusString = "↻" + infoManager.updateInfoData(type: .minMax, value: "N/A") } } - } - } - private func findFallbackEnactedAndSetLoopTime( - in allDeviceStatuses: [[String: AnyObject]], - formatter: ISO8601DateFormatter - ) { - for i in 1 ..< allDeviceStatuses.count { - let ds = allDeviceStatuses[i] - guard - let openaps = ds["openaps"] as? [String: AnyObject], - openaps["failureReason"] == nil, - let enacted = openaps["enacted"] as? [String: AnyObject], - let dateString = ds["created_at"] as? String, - let dateTime = formatter.date(from: dateString)?.timeIntervalSince1970 - else { - continue + 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 && !wasEnacted { + LoopStatusLabel.text = "⏀" + latestLoopStatusString = "⏀" + } else { + LoopStatusLabel.text = "↻" + latestLoopStatusString = "↻" + } + } + } else { + LoopStatusLabel.text = "↻" + latestLoopStatusString = "↻" } - - UserDefaultsRepository.alertLastLoopTime.value = dateTime - LogManager.shared.log(category: .deviceStatus, message: "Found older enacted. Setting lastLoopTime to \(dateTime)", isDebug: true) - - evaluateNotLooping(lastLoopTime: dateTime) - return } - - LogManager.shared.log(category: .deviceStatus, message: "No older record was enacted!") } } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index f0952fe4..026e569f 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -14,7 +14,6 @@ class Observable { var tempTarget = ObservableValue(default: nil) var override = ObservableValue(default: nil) - var isLastDeviceStatusSuggested = ObservableValue(default: false) private init() {} } From ce3b4795426231fa79ea86fc0db5346799f2f7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 28 Jan 2025 18:52:39 +0100 Subject: [PATCH 2/4] Buggfix --- LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index bb682b18..e76e441e 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -25,7 +25,7 @@ extension MainViewController { var wasEnacted : Bool if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] { wasEnacted = true - if let timestampString = lastDeviceStatus?["timestamp"] as? String, + if let timestampString = enacted["timestamp"] as? String, let lastLoopTime = formatter.date(from: timestampString)?.timeIntervalSince1970 { UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime LogManager.shared.log(category: .deviceStatus, message: "New LastLoopTime: \(lastLoopTime)", isDebug: true) From b1bec482b45e1deb2a7b486d2e1072c7bbc3f1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 31 Jan 2025 12:15:11 +0100 Subject: [PATCH 3/4] Modified Not Looping handling --- LoopFollow/Controllers/Nightscout/DeviceStatus.swift | 11 +++++++---- .../Controllers/Nightscout/DeviceStatusLoop.swift | 5 ++--- .../Controllers/Nightscout/DeviceStatusOpenAPS.swift | 4 ---- LoopFollow/ViewControllers/MainViewController.swift | 1 - 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 4b99dc02..085af6f0 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -34,11 +34,13 @@ extension MainViewController { DispatchQueue.main.async { TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(10)) } + + evaluateNotLooping() } - func evaluateNotLooping(lastLoopTime: TimeInterval) { + func evaluateNotLooping() { if let statusStackView = LoopStatusLabel.superview as? UIStackView { - if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 { + if ((TimeInterval(Date().timeIntervalSince1970) - UserDefaultsRepository.alertLastLoopTime.value) / 60) > 15 { IsNotLooping = true // Change the distribution to 'fill' to allow manual resizing of arranged subviews statusStackView.distribution = .fill @@ -70,7 +72,6 @@ extension MainViewController { } } } - latestLoopTime = lastLoopTime } // NS Device Status Response Processor @@ -145,7 +146,7 @@ extension MainViewController { // Start the timer based on the timestamp let now = dateTimeUtils.getNowTimeIntervalUTC() - let secondsAgo = now - latestLoopTime + let secondsAgo = now - UserDefaultsRepository.alertLastLoopTime.value DispatchQueue.main.async { if secondsAgo >= (20 * 60) { @@ -179,6 +180,8 @@ extension MainViewController { ) } } + + evaluateNotLooping() LogManager.shared.log(category: .deviceStatus, message: "Update Device Status done", isDebug: true) } } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 45305766..669f9508 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -15,6 +15,7 @@ extension MainViewController { func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) { ObservableUserDefaults.shared.device.value = "Loop" if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 { + let previousLastLoopTime = UserDefaultsRepository.alertLastLoopTime.value UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime if let failure = lastLoopRecord["failureReason"] { LoopStatusLabel.text = "X" @@ -66,7 +67,7 @@ extension MainViewController { let prediction = predictdata["values"] as! [Double] PredictionLabel.text = Localizer.toDisplayUnits(String(Int(prediction.last!))) PredictionLabel.textColor = UIColor.systemPurple - if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime { + if UserDefaultsRepository.downloadPrediction.value && previousLastLoopTime < lastLoopTime { predictionData.removeAll() var predictionTime = lastLoopTime let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) @@ -123,8 +124,6 @@ extension MainViewController { } } - - evaluateNotLooping(lastLoopTime: lastLoopTime) } } } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index e76e441e..cc2241be 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -13,12 +13,10 @@ extension MainViewController { if lastLoopRecord["failureReason"] != nil { LoopStatusLabel.text = "X" latestLoopStatusString = "X" - evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) } else { guard let enactedOrSuggested = lastLoopRecord["suggested"] as? [String: AnyObject] ?? lastLoopRecord["enacted"] as? [String: AnyObject] else { LoopStatusLabel.text = "↻" latestLoopStatusString = "↻" - evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) return } @@ -34,8 +32,6 @@ extension MainViewController { wasEnacted = false LogManager.shared.log(category: .deviceStatus, message: "Last devicestatus is missing enacted") } - evaluateNotLooping(lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value) - var updatedTime: TimeInterval? diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 095bb1a8..1418dd80 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -108,7 +108,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var latestMinAgoString = "" var latestDeltaString = "" var latestLoopStatusString = "" - var latestLoopTime: Double = 0 var latestCOB: CarbMetric? var latestBasal = "" var latestPumpVolume: Double = 50.0 From e8abd9976ec704ed1f872950d2544a93794b8fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 31 Jan 2025 12:35:10 +0100 Subject: [PATCH 4/4] Align Not Looping display threshold with the alarm if it is enabled --- .../Controllers/Nightscout/DeviceStatus.swift | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 085af6f0..182e13c6 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -39,41 +39,47 @@ extension MainViewController { } func evaluateNotLooping() { - if let statusStackView = LoopStatusLabel.superview as? UIStackView { - if ((TimeInterval(Date().timeIntervalSince1970) - UserDefaultsRepository.alertLastLoopTime.value) / 60) > 15 { - IsNotLooping = true - // Change the distribution to 'fill' to allow manual resizing of arranged subviews - statusStackView.distribution = .fill - - // Hide PredictionLabel and expand LoopStatusLabel to fill the entire stack view - PredictionLabel.isHidden = true - LoopStatusLabel.frame = CGRect(x: 0, y: 0, width: statusStackView.frame.width, height: statusStackView.frame.height) - - // Update LoopStatusLabel's properties to display Not Looping - LoopStatusLabel.textAlignment = .center - LoopStatusLabel.text = "⚠️ Not Looping!" - LoopStatusLabel.textColor = UIColor.systemYellow - LoopStatusLabel.font = UIFont.boldSystemFont(ofSize: 18) - + guard let statusStackView = LoopStatusLabel.superview as? UIStackView else { return } + + let now = TimeInterval(Date().timeIntervalSince1970) + let lastLoopTime = UserDefaultsRepository.alertLastLoopTime.value + let isAlarmEnabled = UserDefaultsRepository.alertNotLoopingActive.value + let nonLoopingTimeThreshold: TimeInterval + + if isAlarmEnabled { + nonLoopingTimeThreshold = Double(UserDefaultsRepository.alertNotLooping.value * 60) + } else { + nonLoopingTimeThreshold = 15 * 60 + } + + if IsNightscoutEnabled(), (now - lastLoopTime) >= nonLoopingTimeThreshold, lastLoopTime > 0 { + IsNotLooping = true + statusStackView.distribution = .fill + + PredictionLabel.isHidden = true + LoopStatusLabel.frame = CGRect(x: 0, y: 0, width: statusStackView.frame.width, height: statusStackView.frame.height) + + LoopStatusLabel.textAlignment = .center + LoopStatusLabel.text = "⚠️ Not Looping!" + LoopStatusLabel.textColor = UIColor.systemYellow + LoopStatusLabel.font = UIFont.boldSystemFont(ofSize: 18) + + } else { + IsNotLooping = false + statusStackView.distribution = .fillEqually + PredictionLabel.isHidden = false + + LoopStatusLabel.textAlignment = .right + LoopStatusLabel.font = UIFont.systemFont(ofSize: 17) + + if UserDefaultsRepository.forceDarkMode.value { + LoopStatusLabel.textColor = UIColor.white } else { - IsNotLooping = false - // Restore the original distribution and visibility of labels - statusStackView.distribution = .fillEqually - PredictionLabel.isHidden = false - - // Reset LoopStatusLabel's properties - LoopStatusLabel.textAlignment = .right - LoopStatusLabel.font = UIFont.systemFont(ofSize: 17) - - if UserDefaultsRepository.forceDarkMode.value { - LoopStatusLabel.textColor = UIColor.white - } else { - LoopStatusLabel.textColor = UIColor.black - } + LoopStatusLabel.textColor = UIColor.black } } } - + // NS Device Status Response Processor func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) { infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio, .updated, .recBolus, .tdd])