瀏覽代碼

Only clear the forecast when the data source changes (#707)

* Only clear the forecast when the data source changes

The refresh handler used to wipe the Loop and Trio forecasts on every refresh, including every websocket disconnect, which made the Loop forecast disappear and only come back on the next loop cycle. Move the clearing so it only happens when the active system actually changes: when a device status flips between Loop and Trio, and when Nightscout is turned off so there is no device status source at all, such as Dexcom only.

* Rebuild the Loop forecast when it is empty

The forecast was only rebuilt when the loop cycle advanced (previousLastLoopTime < lastLoopTime), so any time the prediction was cleared within the same cycle it stayed blank until the next loop cycle. Also rebuild when predictionData is empty, so the forecast is restored on the next device-status poll regardless of cycle timing.

* Fall back to the loop timestamp when the pump reports no clock

A pump that omits pump.clock (e.g. some Omnipod configurations) left alertLastLoopTime at 0, so the Loop forecast anchored to epoch 0 and stretched the graph across decades. Use the loop cycle timestamp as the loop time when no pump clock is present, so the forecast anchors correctly and refreshes each cycle.
Jonas Björkert 1 周之前
父節點
當前提交
290e020f1c

+ 24 - 0
LoopFollow/Controllers/Graphs.swift

@@ -967,6 +967,30 @@ extension MainViewController {
         BGChart.notifyDataSetChanged()
     }
 
+    // Removes the Loop forecast line. Used when the active system switches away from Loop.
+    func clearLoopPredictionGraph() {
+        guard !predictionData.isEmpty else { return }
+        predictionData.removeAll()
+        updatePredictionGraph()
+    }
+
+    // Removes the Trio/OpenAPS forecast lines (ZT/IOB/COB/UAM). Used when the active system switches away from Trio/OpenAPS.
+    func clearOpenAPSPredictionGraph() {
+        let openAPSDataIndices = [12, 13, 14, 15]
+        for dataIndex in openAPSDataIndices {
+            let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
+            let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
+            if !mainChart.entries.isEmpty || !smallChart.entries.isEmpty {
+                updatePredictionGraphGeneric(
+                    dataIndex: dataIndex,
+                    predictionData: [],
+                    chartLabel: "",
+                    color: UIColor.systemGray
+                )
+            }
+        }
+    }
+
     func updatePredictionGraph(color: UIColor? = nil) {
         let dataIndex = 1
         var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet

+ 23 - 0
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -66,6 +66,7 @@ extension MainViewController {
     // NS Device Status Response Processor
     func updateDeviceStatusDisplay(jsonDeviceStatus: [[String: AnyObject]]) {
         let previousIOBText = Observable.shared.iobText.value
+        let previousDeviceWasLoop = Storage.shared.device.value == "Loop"
         infoManager.clearInfoData(types: [.iob, .cob, .battery, .pump, .pumpBattery, .target, .isf, .carbRatio, .updated, .recBolus, .tdd])
 
         // For Loop, clear the current override here - For Trio, it is handled using treatments
@@ -155,6 +156,17 @@ extension MainViewController {
 
         // Loop - handle new data
         if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? {
+            // Some pumps report no `pump.clock`; without it alertLastLoopTime stays 0
+            // and the forecast anchors to epoch 0. Fall back to the loop cycle timestamp.
+            if (lastDeviceStatus?["pump"] as? [String: AnyObject])?["clock"] == nil,
+               let loopTimestampString = lastLoopRecord["timestamp"] as? String,
+               let loopTimestamp = formatter.date(from: loopTimestampString)?.timeIntervalSince1970,
+               loopTimestamp > (Observable.shared.alertLastLoopTime.value ?? 0)
+            {
+                Observable.shared.alertLastLoopTime.value = loopTimestamp
+                Storage.shared.lastLoopTime.value = loopTimestamp
+            }
+
             DeviceStatusLoop(formatter: formatter, lastLoopRecord: lastLoopRecord)
 
             var oText = ""
@@ -188,6 +200,17 @@ extension MainViewController {
             DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord)
         }
 
+        // If the active looping system flipped (Loop ⇄ Trio/OpenAPS), drop the previous
+        // system's forecast so it doesn't linger next to the one just drawn above.
+        let currentDeviceIsLoop = Storage.shared.device.value == "Loop"
+        if currentDeviceIsLoop != previousDeviceWasLoop {
+            if currentDeviceIsLoop {
+                clearOpenAPSPredictionGraph()
+            } else {
+                clearLoopPredictionGraph()
+            }
+        }
+
         // Start the timer based on the timestamp
         let now = dateTimeUtils.getNowTimeIntervalUTC()
         let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0)

+ 1 - 1
LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift

@@ -68,7 +68,7 @@ extension MainViewController {
                 let prediction = predictdata["values"] as! [Double]
                 Observable.shared.predictionText.value = Localizer.toDisplayUnits(String(Int(round(prediction.last!))))
                 Observable.shared.predictionColor.value = .purple
-                if Storage.shared.downloadPrediction.value, previousLastLoopTime < lastLoopTime {
+                if Storage.shared.downloadPrediction.value, previousLastLoopTime < lastLoopTime || predictionData.isEmpty {
                     predictionData.removeAll()
                     var predictionTime = lastLoopTime
                     let toLoad = Int(Storage.shared.predictionToLoad.value * 12)

+ 4 - 0
LoopFollow/Task/DeviceStatusTask.swift

@@ -15,6 +15,10 @@ extension MainViewController {
     func deviceStatusAction() {
         // If no NS config, we wait 60s before trying again:
         guard IsNightscoutEnabled() else {
+            // No device-status source (e.g. Dexcom-only): drop any forecast left over
+            // from a previous Loop/Trio source so it doesn't linger on the chart.
+            clearLoopPredictionGraph()
+            clearOpenAPSPredictionGraph()
             TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(60))
             return
         }

+ 0 - 23
LoopFollow/ViewControllers/MainViewController.swift

@@ -565,29 +565,6 @@ class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificatio
     @objc func refresh() {
         LogManager.shared.log(category: .general, message: "Refreshing")
 
-        // Clear prediction for both Loop or OpenAPS
-
-        // Check if Loop prediction data exists and clear it if necessary
-        if !predictionData.isEmpty {
-            predictionData.removeAll()
-            updatePredictionGraph()
-        }
-
-        // Check if OpenAPS prediction data exists and clear it if necessary
-        let openAPSDataIndices = [12, 13, 14, 15]
-        for dataIndex in openAPSDataIndices {
-            let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
-            let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
-            if !mainChart.entries.isEmpty || !smallChart.entries.isEmpty {
-                updatePredictionGraphGeneric(
-                    dataIndex: dataIndex,
-                    predictionData: [],
-                    chartLabel: "",
-                    color: UIColor.systemGray
-                )
-            }
-        }
-
         Observable.shared.minAgoText.value = "Refreshing"
         scheduleAllTasks()
         NightscoutSocketManager.shared.connectIfNeeded()