Explorar el Código

Merge pull request #304 from loopandlearn/openaps-predictions

Trio predictions
Marion Barker hace 2 años
padre
commit
57e6350f3e

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -21,6 +21,8 @@
 		DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE42ACF2383009A6922 /* Treatments.swift */; };
 		DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; };
 		DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; };
+		DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */; };
+		DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */; };
 		DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19832ACDA50C00DBD158 /* Overrides.swift */; };
 		DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; };
 		DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19872ACDA5DA00DBD158 /* Notes.swift */; };
@@ -211,6 +213,8 @@
 		DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = "<group>"; };
 		DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = "<group>"; };
 		DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = "<group>"; };
+		DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusLoop.swift; sourceTree = "<group>"; };
+		DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusOpenAPS.swift; sourceTree = "<group>"; };
 		DD7E19832ACDA50C00DBD158 /* Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overrides.swift; sourceTree = "<group>"; };
 		DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = "<group>"; };
 		DD7E19872ACDA5DA00DBD158 /* Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notes.swift; sourceTree = "<group>"; };
@@ -450,6 +454,8 @@
 				DD493AE42ACF2383009A6922 /* Treatments.swift */,
 				DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */,
 				DD493AE82ACF2445009A6922 /* BGData.swift */,
+				DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */,
+				DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */,
 			);
 			path = Nightscout;
 			sourceTree = "<group>";
@@ -1013,6 +1019,7 @@
 				DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */,
 				DDCF979C24C14EFB002C9752 /* AdvancedSettingsViewController.swift in Sources */,
 				FC97881C2485969B00A7906C /* MainViewController.swift in Sources */,
+				DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */,
 				DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */,
 				DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */,
 				FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */,
@@ -1021,6 +1028,7 @@
 				DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */,
 				DD493AE92ACF2445009A6922 /* BGData.swift in Sources */,
 				FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */,
+				DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */,
 				DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */,
 				DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,

+ 0 - 8
LoopFollow/Application/Base.lproj/Main.storyboard

@@ -529,14 +529,6 @@
                     <view key="view" contentMode="scaleToFill" id="ljg-dW-5uR">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <subviews>
-                            <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6gS-Ff-qPd">
-                                <rect key="frame" x="0.0" y="44" width="414" height="769"/>
-                                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
-                                <viewLayoutGuide key="contentLayoutGuide" id="1wh-Gg-Auv"/>
-                                <viewLayoutGuide key="frameLayoutGuide" id="lqA-iJ-wdZ"/>
-                            </scrollView>
-                        </subviews>
                         <viewLayoutGuide key="safeArea" id="aVb-ap-Pwt"/>
                         <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                     </view>

+ 229 - 8
LoopFollow/Controllers/Graphs.swift

@@ -254,6 +254,102 @@ extension MainViewController {
         lineNote.axisDependency = YAxis.AxisDependency.right
         lineNote.valueFormatter = ChartYDataValueFormatter()
         lineNote.drawValuesEnabled = false
+
+        // Setup COB Prediction line details
+        let COBpredictionChartEntry = [ChartDataEntry]()
+        let COBlinePrediction = LineChartDataSet(entries:COBpredictionChartEntry, label: "")
+        COBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
+        COBlinePrediction.circleColors = [NSUIColor.systemPurple]
+        COBlinePrediction.colors = [NSUIColor.systemPurple]
+        COBlinePrediction.drawCircleHoleEnabled = false
+        COBlinePrediction.axisDependency = YAxis.AxisDependency.right
+        COBlinePrediction.highlightEnabled = true
+        COBlinePrediction.drawValuesEnabled = false
+        
+        if UserDefaultsRepository.showLines.value {
+            COBlinePrediction.lineWidth = 2
+        } else {
+            COBlinePrediction.lineWidth = 0
+        }
+        if UserDefaultsRepository.showDots.value {
+            COBlinePrediction.drawCirclesEnabled = true
+        } else {
+            COBlinePrediction.drawCirclesEnabled = false
+        }
+        COBlinePrediction.setDrawHighlightIndicators(false)
+        COBlinePrediction.valueFont.withSize(50)
+        
+        // Setup IOB Prediction line details
+        let IOBpredictionChartEntry = [ChartDataEntry]()
+        let IOBlinePrediction = LineChartDataSet(entries:IOBpredictionChartEntry, label: "")
+        IOBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
+        IOBlinePrediction.circleColors = [NSUIColor.systemPurple]
+        IOBlinePrediction.colors = [NSUIColor.systemPurple]
+        IOBlinePrediction.drawCircleHoleEnabled = false
+        IOBlinePrediction.axisDependency = YAxis.AxisDependency.right
+        IOBlinePrediction.highlightEnabled = true
+        IOBlinePrediction.drawValuesEnabled = false
+        
+        if UserDefaultsRepository.showLines.value {
+            IOBlinePrediction.lineWidth = 2
+        } else {
+            IOBlinePrediction.lineWidth = 0
+        }
+        if UserDefaultsRepository.showDots.value {
+            IOBlinePrediction.drawCirclesEnabled = true
+        } else {
+            IOBlinePrediction.drawCirclesEnabled = false
+        }
+        IOBlinePrediction.setDrawHighlightIndicators(false)
+        IOBlinePrediction.valueFont.withSize(50)
+        
+        // Setup UAM Prediction line details
+        let UAMpredictionChartEntry = [ChartDataEntry]()
+        let UAMlinePrediction = LineChartDataSet(entries:UAMpredictionChartEntry, label: "")
+        UAMlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
+        UAMlinePrediction.circleColors = [NSUIColor.systemPurple]
+        UAMlinePrediction.colors = [NSUIColor.systemPurple]
+        UAMlinePrediction.drawCircleHoleEnabled = false
+        UAMlinePrediction.axisDependency = YAxis.AxisDependency.right
+        UAMlinePrediction.highlightEnabled = true
+        UAMlinePrediction.drawValuesEnabled = false
+        
+        if UserDefaultsRepository.showLines.value {
+            UAMlinePrediction.lineWidth = 2
+        } else {
+            UAMlinePrediction.lineWidth = 0
+        }
+        if UserDefaultsRepository.showDots.value {
+            UAMlinePrediction.drawCirclesEnabled = true
+        } else {
+            UAMlinePrediction.drawCirclesEnabled = false
+        }
+        linePrediction.setDrawHighlightIndicators(false)
+        linePrediction.valueFont.withSize(50)
+        
+        // Setup ZT Prediction line details
+        let ZTpredictionChartEntry = [ChartDataEntry]()
+        let ZTlinePrediction = LineChartDataSet(entries:ZTpredictionChartEntry, label: "")
+        ZTlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
+        ZTlinePrediction.circleColors = [NSUIColor.systemPurple]
+        ZTlinePrediction.colors = [NSUIColor.systemPurple]
+        ZTlinePrediction.drawCircleHoleEnabled = false
+        ZTlinePrediction.axisDependency = YAxis.AxisDependency.right
+        ZTlinePrediction.highlightEnabled = true
+        ZTlinePrediction.drawValuesEnabled = false
+        
+        if UserDefaultsRepository.showLines.value {
+            ZTlinePrediction.lineWidth = 2
+        } else {
+            ZTlinePrediction.lineWidth = 0
+        }
+        if UserDefaultsRepository.showDots.value {
+            ZTlinePrediction.drawCirclesEnabled = true
+        } else {
+            ZTlinePrediction.drawCirclesEnabled = false
+        }
+        ZTlinePrediction.setDrawHighlightIndicators(false)
+        ZTlinePrediction.valueFont.withSize(50)
         
         // Setup the chart data of all lines
         let data = LineChartData()
@@ -270,7 +366,11 @@ extension MainViewController {
         data.append(lineResume) // Dataset 9
         data.append(lineSensor) // Dataset 10
         data.append(lineNote) // Dataset 11
-        
+        data.append(ZTlinePrediction) // Dataset 12
+        data.append(IOBlinePrediction) // Dataset 13
+        data.append(COBlinePrediction) // Dataset 14
+        data.append(UAMlinePrediction) // Dataset 15
+
         data.setValueFont(UIFont.systemFont(ofSize: 12))
         
         // Add marker popups for bolus and carbs
@@ -1137,6 +1237,58 @@ extension MainViewController {
         lineNote.axisDependency = YAxis.AxisDependency.right
         lineNote.valueFormatter = ChartYDataValueFormatter()
         lineNote.drawValuesEnabled = false
+
+        // Setup COB Prediction line details
+        var COBpredictionChartEntry = [ChartDataEntry]()
+        let COBlinePrediction = LineChartDataSet(entries:COBpredictionChartEntry, label: "")
+        COBlinePrediction.drawCirclesEnabled = false
+        COBlinePrediction.setColor(NSUIColor.systemPurple)
+        COBlinePrediction.highlightEnabled = true
+        COBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
+        COBlinePrediction.drawVerticalHighlightIndicatorEnabled = false
+        COBlinePrediction.highlightColor = NSUIColor.label
+        COBlinePrediction.drawValuesEnabled = false
+        COBlinePrediction.lineWidth = 1.5
+        COBlinePrediction.axisDependency = YAxis.AxisDependency.right
+
+        // Setup IOB Prediction line details
+        var IOBpredictionChartEntry = [ChartDataEntry]()
+        let IOBlinePrediction = LineChartDataSet(entries:IOBpredictionChartEntry, label: "")
+        IOBlinePrediction.drawCirclesEnabled = false
+        IOBlinePrediction.setColor(NSUIColor.systemPurple)
+        IOBlinePrediction.highlightEnabled = true
+        IOBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
+        IOBlinePrediction.drawVerticalHighlightIndicatorEnabled = false
+        IOBlinePrediction.highlightColor = NSUIColor.label
+        IOBlinePrediction.drawValuesEnabled = false
+        IOBlinePrediction.lineWidth = 1.5
+        IOBlinePrediction.axisDependency = YAxis.AxisDependency.right
+
+        // Setup UAM Prediction line details
+        var UAMpredictionChartEntry = [ChartDataEntry]()
+        let UAMlinePrediction = LineChartDataSet(entries:UAMpredictionChartEntry, label: "")
+        UAMlinePrediction.drawCirclesEnabled = false
+        UAMlinePrediction.setColor(NSUIColor.systemPurple)
+        UAMlinePrediction.highlightEnabled = true
+        UAMlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
+        UAMlinePrediction.drawVerticalHighlightIndicatorEnabled = false
+        UAMlinePrediction.highlightColor = NSUIColor.label
+        UAMlinePrediction.drawValuesEnabled = false
+        UAMlinePrediction.lineWidth = 1.5
+        UAMlinePrediction.axisDependency = YAxis.AxisDependency.right
+
+        // Setup ZT Prediction line details
+        var ZTpredictionChartEntry = [ChartDataEntry]()
+        let ZTlinePrediction = LineChartDataSet(entries:ZTpredictionChartEntry, label: "")
+        ZTlinePrediction.drawCirclesEnabled = false
+        ZTlinePrediction.setColor(NSUIColor.systemPurple)
+        ZTlinePrediction.highlightEnabled = true
+        ZTlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
+        ZTlinePrediction.drawVerticalHighlightIndicatorEnabled = false
+        ZTlinePrediction.highlightColor = NSUIColor.label
+        ZTlinePrediction.drawValuesEnabled = false
+        ZTlinePrediction.lineWidth = 1.5
+        ZTlinePrediction.axisDependency = YAxis.AxisDependency.right
         
         // Setup the chart data of all lines
         let data = LineChartData()
@@ -1152,7 +1304,11 @@ extension MainViewController {
         data.append(lineResume) // Dataset 9
         data.append(lineSensor) // Dataset 10
         data.append(lineNote) // Dataset 11
-        
+        data.append(ZTlinePrediction) // Dataset 12
+        data.append(IOBlinePrediction) // Dataset 13
+        data.append(COBlinePrediction) // Dataset 14
+        data.append(UAMlinePrediction) // Dataset 15
+
         BGChartFull.highlightPerDragEnabled = true
         BGChartFull.leftAxis.enabled = false
         BGChartFull.leftAxis.axisMaximum = maxBasal
@@ -1237,21 +1393,86 @@ extension MainViewController {
         }
     }
     
-    func formatPillText(line1: String, time: TimeInterval) -> String {
+    func formatPillText(line1: String, time: TimeInterval, line2: String? = nil) -> String {
         let dateFormatter = DateFormatter()
-        //let timezoneOffset = TimeZone.current.secondsFromGMT()
-        //let epochTimezoneOffset = value + Double(timezoneOffset)
         if dateTimeUtils.is24Hour() {
             dateFormatter.setLocalizedDateFormatFromTemplate("HH:mm")
         } else {
             dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm")
         }
         
-        //let date = Date(timeIntervalSince1970: epochTimezoneOffset)
         let date = Date(timeIntervalSince1970: time)
         let formattedDate = dateFormatter.string(from: date)
-
-        return line1 + "\r\n" + formattedDate
+        
+        if let line2 = line2 {
+            return line1 + "\r\n" + line2 + "\r\n" + formattedDate
+        } else {
+            return line1 + "\r\n" + formattedDate
+        }
     }
   
+    func updatePredictionGraphGeneric(
+        dataIndex: Int,
+        predictionData: [ShareGlucoseData],
+        chartLabel: String,
+        color: UIColor
+    ) {
+        let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
+        let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
+        mainChart.clear()
+        smallChart.clear()
+        
+        var colors = [NSUIColor]()
+        let maxBGOffset: Float = 20
+        
+        for i in 0..<predictionData.count {
+            let predictionVal = Double(predictionData[i].sgv)
+            if Float(predictionVal) > topBG - maxBGOffset {
+                topBG = Float(predictionVal) + maxBGOffset
+            }
+            
+            if i == 0 {
+                if UserDefaultsRepository.showDots.value {
+                    colors.append((color).withAlphaComponent(0.0))
+                } else {
+                    colors.append((color).withAlphaComponent(1.0))
+                }
+            } else {
+                colors.append(color)
+            }
+            
+            let value = ChartDataEntry(
+                x: predictionData[i].date,
+                y: predictionVal,
+                data: formatPillText(
+                    line1: chartLabel,
+                    time: predictionData[i].date,
+                    line2: bgUnits.toDisplayUnits(String(predictionVal))
+                )
+            )
+            mainChart.addEntry(value)
+            smallChart.addEntry(value)
+        }
+        
+        smallChart.circleColors.removeAll()
+        smallChart.colors.removeAll()
+        mainChart.colors.removeAll()
+        mainChart.circleColors.removeAll()
+        if colors.count > 0 {
+            for color in colors {
+                mainChart.addColor(color)
+                mainChart.circleColors.append(color)
+                smallChart.addColor(color)
+                smallChart.circleColors.append(color)
+            }
+        }
+        
+        BGChart.rightAxis.axisMaximum = Double(topBG)
+        BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
+        BGChart.data?.notifyDataChanged()
+        BGChart.notifyDataSetChanged()
+        BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
+        BGChartFull.data?.notifyDataChanged()
+        BGChartFull.notifyDataSetChanged()
+    }
 }

+ 6 - 223
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -8,6 +8,7 @@
 
 import Foundation
 import UIKit
+import Charts
 
 extension MainViewController {
     // NS Device Status Web Call
@@ -123,232 +124,14 @@ extension MainViewController {
             }
         }
         
-        // Loop
+        // Loop - handle new data
         if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
-            //print("Loop: \(lastLoopRecord)")
-            if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970  {
-                UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
-                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
-                if let failure = lastLoopRecord["failureReason"] {
-                    LoopStatusLabel.text = "X"
-                    latestLoopStatusString = "X"
-                    if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
-                } else {
-                    var wasEnacted = false
-                    if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
-                        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
-                        wasEnacted = true
-                        if let lastTempBasal = enacted["rate"] as? Double {
-                            
-                        }
-                    }
-                    if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
-                        tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
-                        latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
-                    }
-                    if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
-                        tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
-                        latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
-                    }
-                    if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
-                        let prediction = predictdata["values"] as! [Double]
-                        PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
-                        PredictionLabel.textColor = UIColor.systemPurple
-                        if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
-                            predictionData.removeAll()
-                            var predictionTime = lastLoopTime
-                            let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
-                            var i = 0
-                            while i <= toLoad {
-                                if i < prediction.count {
-                                    let sgvValue = Int(round(prediction[i]))
-                                    // Skip values higher than 600
-                                    if sgvValue <= 600 {
-                                        let prediction = ShareGlucoseData(sgv: sgvValue, date: predictionTime, direction: "flat")
-                                        predictionData.append(prediction)
-                                    }
-                                    predictionTime += 300
-                                }
-                                i += 1
-                            }
-                            
-                            let predMin = prediction.min()
-                            let predMax = prediction.max()
-                            tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
-                            
-                            updatePredictionGraph()
-                        }
-                    }
-                    if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
-                        tableData[8].value = String(format:"%.2fU", recBolus)
-                        UserDefaultsRepository.deviceRecBolus.value = recBolus
-                    }
-                    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 UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
-                            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
-                            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
-                            if tempBasalTime > lastBGTime && !wasEnacted {
-                                LoopStatusLabel.text = "⏀"
-                                latestLoopStatusString = "⏀"
-                                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
-                            } else {
-                                LoopStatusLabel.text = "↻"
-                                latestLoopStatusString = "↻"
-                                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
-                            }
-                        }
-                    } else {
-                        LoopStatusLabel.text = "↻"
-                        latestLoopStatusString = "↻"
-                        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
-                    }
-                    
-                }
+            DeviceStatusLoop(formatter: formatter, lastLoopRecord: lastLoopRecord)
+        }
 
-                evaluateNotLooping(lastLoopTime: lastLoopTime)
-            } // end lastLoopTime
-        } // end lastLoop Record
-        
+        // OpenAPS - handle new data
         if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String : AnyObject]? {
-            if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970  {
-                UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
-                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
-                if let failure = lastLoopRecord["failureReason"] {
-                    LoopStatusLabel.text = "X"
-                    latestLoopStatusString = "X"
-                    if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
-                } else {
-                    var wasEnacted = false
-                    if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
-                        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
-                        wasEnacted = true
-                        if let lastTempBasal = enacted["rate"] as? Double {
-                            
-                        }
-                    }
-                    
-                    if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
-                        tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
-                        latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
-                    }
-                    if let cobdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
-                        tableData[1].value = String(format:"%.0f", cobdata["COB"] as! Double)
-                        latestCOB = String(format:"%.0f", cobdata["COB"] as! Double)
-                    }
-                    if let recbolusdata = lastLoopRecord["enacted"] as? [String: AnyObject],
-                       let insulinReq = recbolusdata["insulinReq"] as? Double {
-                        tableData[8].value = String(format: "%.2fU", insulinReq)
-                        UserDefaultsRepository.deviceRecBolus.value = insulinReq
-                    } else {
-                        tableData[8].value = "N/A"
-                        UserDefaultsRepository.deviceRecBolus.value = 0
-                        print("Warning: Failed to extract insulinReq from recbolusdata.")
-                    }
-
-                    if let autosensdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
-                        let sens = autosensdata["sensitivityRatio"] as! Double * 100.0
-                        tableData[11].value = String(format:"%.0f", sens) + "%"
-                    }
-                    
-                    //Picks COB prediction if available, else UAM, else IOB, else ZT
-                    //Ideal is to predict all 4 in Loop Follow but this is a quick start
-                    var graphtype = ""
-                    var predictioncolor = UIColor.systemGray
-                    PredictionLabel.textColor = predictioncolor
-
-                    if let enactdata = lastLoopRecord["enacted"] as? [String:AnyObject],
-                       let predbgdata = enactdata["predBGs"] as? [String:AnyObject] {
-
-                        if predbgdata["COB"] != nil {
-                            graphtype = "COB"
-                        } else if predbgdata["UAM"] != nil {
-                            graphtype = "UAM"
-                        } else if predbgdata["IOB"] != nil {
-                            graphtype = "IOB"
-                        } else {
-                            graphtype = "ZT"
-                        }
-
-                        // Access the color based on graphtype
-                        var colorName = ""
-                        switch graphtype {
-                        case "COB": colorName = "LoopYellow"
-                        case "UAM": colorName = "UAM"
-                        case "IOB": colorName = "Insulin"
-                        case "ZT": colorName = "ZT"
-                        default: break
-                        }
-
-                        if let selectedColor = UIColor(named: colorName) {
-                            predictioncolor = selectedColor
-                            PredictionLabel.textColor = predictioncolor
-                        }
-
-                        let graphdata = predbgdata[graphtype] as! [Double]
-
-                        if let eventualdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
-                            if let eventualBGValue = eventualdata["eventualBG"] as? NSNumber {
-                                let eventualBGStringValue = String(describing: eventualBGValue)
-                                PredictionLabel.text = bgUnits.toDisplayUnits(eventualBGStringValue)
-                            }
-                        }
-
-                        if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
-                            predictionData.removeAll()
-                            var predictionTime = lastLoopTime
-                            let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
-                            var i = 0
-                            while i <= toLoad {
-                                if i < graphdata.count {
-                                    let prediction = ShareGlucoseData(sgv: Int(round(graphdata[i])), date: predictionTime, direction: "flat")
-                                    predictionData.append(prediction)
-                                    predictionTime += 300
-                                }
-                                i += 1
-                            }
-                        }
-
-                        let predMin = graphdata.min()
-                        let predMax = graphdata.max()
-                        tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
-
-                        updatePredictionGraph(color: predictioncolor)
-                    }
-                    
-                    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 UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
-                            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
-                            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
-                            if tempBasalTime > lastBGTime && !wasEnacted {
-                                LoopStatusLabel.text = "⏀"
-                                latestLoopStatusString = "⏀"
-                                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
-                            } else {
-                                LoopStatusLabel.text = "↻"
-                                latestLoopStatusString = "↻"
-                                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
-                            }
-                        }
-                    } else {
-                        LoopStatusLabel.text = "↻"
-                        latestLoopStatusString = "↻"
-                        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
-                    }
-                    
-                }
-
-                evaluateNotLooping(lastLoopTime: lastLoopTime)
-            }
+            DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord)
         }
         
         var oText = "" as String

+ 107 - 0
LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift

@@ -0,0 +1,107 @@
+//
+//  DeviceStatusLoop.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-06-16.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import UIKit
+import Charts
+
+extension MainViewController {
+    func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) {
+        
+        if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970  {
+            UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
+            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
+            if let failure = lastLoopRecord["failureReason"] {
+                LoopStatusLabel.text = "X"
+                latestLoopStatusString = "X"
+                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
+            } else {
+                var wasEnacted = false
+                if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
+                    if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
+                    wasEnacted = true
+                    if let lastTempBasal = enacted["rate"] as? Double {
+                        
+                    }
+                }
+                if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
+                    tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
+                    latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
+                }
+                if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
+                    tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
+                    latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
+                }
+                if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
+                    let prediction = predictdata["values"] as! [Double]
+                    PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
+                    PredictionLabel.textColor = UIColor.systemPurple
+                    if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
+                        predictionData.removeAll()
+                        var predictionTime = lastLoopTime
+                        let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
+                        var i = 0
+                        while i <= toLoad {
+                            if i < prediction.count {
+                                let sgvValue = Int(round(prediction[i]))
+                                // Skip values higher than 600
+                                if sgvValue <= 600 {
+                                    let prediction = ShareGlucoseData(sgv: sgvValue, date: predictionTime, direction: "flat")
+                                    predictionData.append(prediction)
+                                }
+                                predictionTime += 300
+                            }
+                            i += 1
+                        }
+                        
+                        let predMin = prediction.min()
+                        let predMax = prediction.max()
+                        tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
+                        
+                        updatePredictionGraph()
+                    }
+                } else {
+                    predictionData.removeAll()
+                    tableData[9].value = ""
+                    updatePredictionGraph()
+                }
+                if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
+                    tableData[8].value = String(format:"%.2fU", recBolus)
+                    UserDefaultsRepository.deviceRecBolus.value = recBolus
+                }
+                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 UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
+                        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
+                        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
+                        if tempBasalTime > lastBGTime && !wasEnacted {
+                            LoopStatusLabel.text = "⏀"
+                            latestLoopStatusString = "⏀"
+                            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
+                        } else {
+                            LoopStatusLabel.text = "↻"
+                            latestLoopStatusString = "↻"
+                            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
+                        }
+                    }
+                } else {
+                    LoopStatusLabel.text = "↻"
+                    latestLoopStatusString = "↻"
+                    if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
+                }
+                
+            }
+            
+            evaluateNotLooping(lastLoopTime: lastLoopTime)
+        }
+    }
+}

+ 84 - 0
LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift

@@ -0,0 +1,84 @@
+//
+//  DeviceStatusOpenAPS.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-05-19.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+extension MainViewController {
+    func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) {
+        
+        if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970  {
+            UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
+            if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
+            if lastLoopRecord["failureReason"] != nil {
+                LoopStatusLabel.text = "X"
+                latestLoopStatusString = "X"
+                if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
+            } else {
+                if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
+                    tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
+                    latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
+                }
+                if let cobdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
+                    tableData[1].value = String(format:"%.0f", cobdata["COB"] as! Double)
+                    latestCOB = String(format:"%.0f", cobdata["COB"] as! Double)
+                }
+                if let recbolusdata = lastLoopRecord["enacted"] as? [String: AnyObject],
+                   let insulinReq = recbolusdata["insulinReq"] as? Double {
+                    tableData[8].value = String(format: "%.2fU", insulinReq)
+                    UserDefaultsRepository.deviceRecBolus.value = insulinReq
+                } else {
+                    tableData[8].value = "N/A"
+                    UserDefaultsRepository.deviceRecBolus.value = 0
+                }
+                
+                if let autosensdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
+                    let sens = autosensdata["sensitivityRatio"] as! Double * 100.0
+                    tableData[11].value = String(format:"%.0f", sens) + "%"
+                }
+                
+                var predictioncolor = UIColor.systemGray
+                PredictionLabel.textColor = predictioncolor
+                
+                if let enactdata = lastLoopRecord["enacted"] as? [String:AnyObject],
+                   let predbgdata = enactdata["predBGs"] as? [String: AnyObject] {
+                    let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [
+                        ("ZT", "ZT", 12),
+                        ("IOB", "Insulin", 13),
+                        ("COB", "LoopYellow", 14),
+                        ("UAM", "UAM", 15)
+                    ]
+                    
+                    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)
+                            
+                            for i in 0...toLoad {
+                                if i < graphdata.count {
+                                    let prediction = ShareGlucoseData(sgv: Int(round(graphdata[i])), 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
+                        )
+                    }
+                }
+            }
+        }
+    }
+}

+ 24 - 0
LoopFollow/ViewControllers/MainViewController.swift

@@ -274,6 +274,30 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     // Clean all timers and start new ones when refreshing
     @objc func refresh() {
         print("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
+                )
+            }
+        }
+
         MinAgoText.text = "Refreshing"
         invalidateTimers()
         restartAllTimers()