Parcourir la source

Merge pull request #197 from jonfawcett/dev

Upgrades for LoopFollow
Marion Barker il y a 3 ans
Parent
commit
6a80177529

+ 4 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -8,6 +8,7 @@
 
 /* Begin PBXBuildFile section */
 		3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; };
+		DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; };
 		DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */; };
 		DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */; };
 		DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; };
@@ -174,6 +175,7 @@
 /* Begin PBXFileReference section */
 		059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = "<group>"; };
 		A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = "<group>"; };
 		DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDisplaySettingsViewController.swift; sourceTree = "<group>"; };
 		DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = "<group>"; };
 		DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = "<group>"; };
@@ -603,6 +605,7 @@
 				FC1BDD2E24A232A3001B652C /* DataStructs.swift */,
 				FCD2A27C24C9D044009F7B7B /* Globals.swift */,
 				FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */,
+				DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */,
 			);
 			path = helpers;
 			sourceTree = "<group>";
@@ -866,6 +869,7 @@
 				FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */,
 				FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */,
 				FC16A97A24996673003D6245 /* NightScout.swift in Sources */,
+				DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */,
 				FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */,
 				FC16A97D24996747003D6245 /* Alarms.swift in Sources */,
 				FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */,

+ 75 - 1
LoopFollow/Controllers/NightScout.swift

@@ -9,10 +9,14 @@
 import Foundation
 import UIKit
 
-
 extension MainViewController {
 
     
+    //NS Carbs Struct
+    struct carbsData: Codable {
+        var carbs: Double?
+    }
+
     //NS Cage Struct
     struct cageData: Codable {
         var created_at: String
@@ -665,6 +669,76 @@ extension MainViewController {
         }
     }
     
+    // NS Carbs Today Web Call
+    func webLoadNSCarbsToday() {
+        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Carbs Today") }
+        let urlUser = UserDefaultsRepository.url.value
+
+        let now = Date()
+        let timeZone = TimeZone.current
+
+        var calendar = Calendar(identifier: .gregorian)
+        calendar.timeZone = timeZone
+
+        var dateComponents = calendar.dateComponents(in: timeZone, from: now)
+        dateComponents.hour = 0
+        dateComponents.minute = 0
+        dateComponents.second = 0
+
+        guard let date = dateComponents.date else { fatalError("Invalid date components") }
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
+        dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
+        let utcDateString = dateFormatter.string(from: date)
+
+        var urlString = urlUser + "/api/v1/treatments.json?count=1000&find[eventType]=Carb+Correction&find[timestamp][$gte]=" + utcDateString
+        if token != "" {
+            urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&count=1000&find[eventType]=Carb+Correction&find[timestamp][$gte]=" + utcDateString
+        }
+
+        guard let urlData = URL(string: urlString) else {
+            return
+        }
+        var request = URLRequest(url: urlData)
+        request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            guard error == nil else {
+                return
+            }
+            guard let data = data else {
+                return
+            }
+
+            let decoder = JSONDecoder()
+            let entriesResponse = try? decoder.decode([carbsData].self, from: data)
+            if let entriesResponse = entriesResponse {
+                DispatchQueue.main.async {
+                    self.updateCarbsToday(data: entriesResponse)
+                }
+            } else {
+                return
+            }
+        }
+        task.resume()
+    }
+
+    // NS CarbsToday Response Processor
+    func updateCarbsToday(data: [carbsData]) {
+        self.clearLastInfoData(index: 10)
+        if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: carbs") }
+        if data.count == 0 {
+            return
+        }
+
+        let totalCarbs = data.reduce(0.0) { $0 + ($1.carbs ?? 0.0) }
+        let resultString = String(format: "%.0f", totalCarbs)
+
+        tableData[10].value = resultString
+
+        infoTable.reloadData()
+    }
+
     // NS Cage Web Call
     func webLoadNSCage() {
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: CAGE") }

+ 11 - 0
LoopFollow/Controllers/Timers.swift

@@ -23,6 +23,16 @@ extension MainViewController {
         if !alarmTimer.isValid { startAlarmTimer(time: 30) }
     }
     
+    func invalidateTimers() {
+        bgTimer.invalidate()
+        profileTimer.invalidate()
+        deviceStatusTimer.invalidate()
+        treatmentsTimer.invalidate()
+        cageSageTimer.invalidate()
+        minAgoTimer.invalidate()
+        calendarTimer.invalidate()
+        alarmTimer.invalidate()
+    }
     
     // min Ago Timer
     func startMinAgoTimer(time: TimeInterval) {
@@ -239,6 +249,7 @@ extension MainViewController {
         }
         
         if UserDefaultsRepository.url.value != "" {
+            webLoadNSCarbsToday()
             webLoadNSCage()
             webLoadNSSage()
             startCageSageTimer()

+ 49 - 4
LoopFollow/ViewControllers/MainViewController.swift

@@ -13,7 +13,7 @@ import ShareClient
 import UserNotifications
 import Photos
 
-class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate {
+class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate {
     
     @IBOutlet weak var BGText: UILabel!
     @IBOutlet weak var DeltaText: UILabel!
@@ -36,7 +36,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     @IBOutlet weak var serverText: UILabel!
     @IBOutlet weak var statsView: UIView!
     @IBOutlet weak var smallGraphHeightConstraint: NSLayoutConstraint!
-    
+    var refreshScrollView: UIScrollView!
+    var refreshControl: UIRefreshControl!
+
     let speechSynthesizer = AVSpeechSynthesizer()
 
     // Data Table class
@@ -160,7 +162,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         UserDefaultsRepository.infoNames.value.append("CAGE")
         UserDefaultsRepository.infoNames.value.append("Rec. Bolus")
         UserDefaultsRepository.infoNames.value.append("Pred.")
-        
+        UserDefaultsRepository.infoNames.value.append("Carbs today")
+
         // Reset deprecated settings
         UserDefaultsRepository.debugLog.value = false;
         UserDefaultsRepository.alwaysDownloadAllBG.value = true;
@@ -232,10 +235,49 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         // Load Startup Data
         restartAllTimers()
         
+        // Set up refreshScrollView for BGText
+        refreshScrollView = UIScrollView()
+        refreshScrollView.translatesAutoresizingMaskIntoConstraints = false
+        refreshScrollView.alwaysBounceVertical = true
+        view.addSubview(refreshScrollView)
+        
+        NSLayoutConstraint.activate([
+            refreshScrollView.leadingAnchor.constraint(equalTo: BGText.leadingAnchor),
+            refreshScrollView.trailingAnchor.constraint(equalTo: BGText.trailingAnchor),
+            refreshScrollView.topAnchor.constraint(equalTo: BGText.topAnchor),
+            refreshScrollView.bottomAnchor.constraint(equalTo: BGText.bottomAnchor)
+        ])
+        
+        refreshControl = UIRefreshControl()
+        refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
+        refreshScrollView.addSubview(refreshControl)
+        
+        // Add this line to prevent scrolling in other directions
+        refreshScrollView.alwaysBounceVertical = true
+        
+        refreshScrollView.delegate = self
     }
     
-
+    // Clean all timers and start new ones when refreshing
+    @objc func refresh() {
+        print("Refreshing")
+        MinAgoText.text = "Refreshing"
+        invalidateTimers()
+        restartAllTimers()
+        refreshControl.endRefreshing()
+    }
     
+    // Scroll down BGText when refreshing
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if scrollView == refreshScrollView {
+            let yOffset = scrollView.contentOffset.y
+            if yOffset < 0 {
+                BGText.transform = CGAffineTransform(translationX: 0, y: -yOffset)
+            } else {
+                BGText.transform = CGAffineTransform.identity
+            }
+        }
+    }
     
     override func viewWillAppear(_ animated: Bool) {
         // set screen lock
@@ -305,6 +347,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     private func createDerivedData() {
         
         self.derivedTableData = []
+        while UserDefaultsRepository.infoVisible.value.count < self.tableData.count {
+            UserDefaultsRepository.infoVisible.value.append(true)
+        }
         for i in 0..<self.tableData.count {
             if(UserDefaultsRepository.infoVisible.value[UserDefaultsRepository.infoSort.value[i]]) {
                 self.derivedTableData.append(self.tableData[UserDefaultsRepository.infoSort.value[i]])

+ 83 - 43
LoopFollow/ViewControllers/SettingsViewController.swift

@@ -14,7 +14,8 @@ import EventKitUI
 class SettingsViewController: FormViewController {
 
    var appStateController: AppStateController?
-    
+    var statusLabelRow: LabelRow!
+
     func showHideNSDetails() {
         var isHidden = false
         var isEnabled = true
@@ -70,45 +71,56 @@ class SettingsViewController: FormViewController {
                guard let value = row.value else { return }
                UserDefaultsRepository.showNS.value = value
        }
-        <<< TextRow(){ row in
-            row.title = "URL"
-            row.placeholder = "https://mycgm.herokuapp.com"
-            row.value = UserDefaultsRepository.url.value
-            row.hidden = "$showNS == false"
-        }.cellSetup { (cell, row) in
-            cell.textField.autocorrectionType = .no
-        }.onChange { row in
-            guard let value = row.value else {
-                UserDefaultsRepository.url.value = ""
-                self.showHideNSDetails()
-                return }
-            // check the format of the URL entered by the user and trim away any spaces or "/" at the end
-            var urlNSInput = value.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
-            if urlNSInput.last == "/" {
-                urlNSInput = String(urlNSInput.dropLast())
-            }
-            UserDefaultsRepository.url.value = urlNSInput.lowercased()
-            // set the row value back to the correctly formatted URL so that the user immediately sees how it should have been written
-            row.value = UserDefaultsRepository.url.value
-            self.showHideNSDetails()
-            globalVariables.nsVerifiedAlert = 0
-            }
-        <<< TextRow(){ row in
-            row.title = "NS Token"
-            row.placeholder = "Leave blank if not using tokens"
-            row.value = UserDefaultsRepository.token.value
-            row.hidden = "$showNS == false"
-        }.cellSetup { (cell, row) in
-            cell.textField.autocorrectionType = .no
-        }.onChange { row in
-            if row.value == nil {
-                UserDefaultsRepository.token.value = ""
-            }
-            guard let value = row.value else { return }
-            UserDefaultsRepository.token.value = value
-            globalVariables.nsVerifiedAlert = 0
-        }
-       
+       <<< TextRow() { row in
+           row.title = "URL"
+           row.placeholder = "https://mycgm.herokuapp.com"
+           row.value = UserDefaultsRepository.url.value
+           row.hidden = "$showNS == false"
+       }.cellSetup { (cell, row) in
+           cell.textField.autocorrectionType = .no
+       }.onChange { row in
+           guard let value = row.value else {
+               UserDefaultsRepository.url.value = ""
+               self.showHideNSDetails()
+               return }
+           // check the format of the URL entered by the user and trim away any spaces or "/" at the end
+           var urlNSInput = value.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
+           if urlNSInput.last == "/" {
+               urlNSInput = String(urlNSInput.dropLast())
+           }
+           UserDefaultsRepository.url.value = urlNSInput.lowercased()
+           // set the row value back to the correctly formatted URL so that the user immediately sees how it should have been written
+           row.value = UserDefaultsRepository.url.value
+           self.showHideNSDetails()
+           globalVariables.nsVerifiedAlert = 0
+           
+           // Verify Nightscout URL and token
+           self.checkNightscoutStatus()
+       }
+       <<< TextRow() { row in
+           row.title = "NS Token"
+           row.placeholder = "Leave blank if not using tokens"
+           row.value = UserDefaultsRepository.token.value
+           row.hidden = "$showNS == false"
+       }.cellSetup { (cell, row) in
+           cell.textField.autocorrectionType = .no
+       }.onChange { row in
+           if row.value == nil {
+               UserDefaultsRepository.token.value = ""
+           }
+           guard let value = row.value else { return }
+           UserDefaultsRepository.token.value = value
+           globalVariables.nsVerifiedAlert = 0
+           
+           // Verify Nightscout URL and token
+           self.checkNightscoutStatus()
+       }
+       <<< LabelRow() { row in
+           row.title = "NS Status"
+           row.value = "Checking..."
+           statusLabelRow = row
+           row.hidden = "$showNS == false"
+       }
        <<< SwitchRow("loopUser"){ row in
            row.title = "Download Loop/FreeAPS Data"
            row.tag = "loopUser"
@@ -240,10 +252,38 @@ class SettingsViewController: FormViewController {
             +++ Section(header: "App Expiration", footer: String(expiration.description))
     
         showHideNSDetails()
-      
-    
+       checkNightscoutStatus()
     }
     
+    func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
+        if let error = error {
+            switch error {
+            case .invalidURL:
+                statusLabelRow.value = "Invalid URL"
+            case .networkError:
+                statusLabelRow.value = "Network Error"
+            case .invalidToken:
+                statusLabelRow.value = "Invalid Token"
+            case .tokenRequired:
+                statusLabelRow.value = "Token Required"
+            case .siteNotFound:
+                statusLabelRow.value = "Site Not Found"
+            case .unknown:
+                statusLabelRow.value = "Unknown Error"
+            case .emptyAddress:
+                statusLabelRow.value = "Address Empty"
+            }
+        } else {
+            statusLabelRow.value = "OK"
+        }
+        statusLabelRow.updateCell()
+    }
     
-
+    func checkNightscoutStatus() {
+        NightscoutUtils.verifyURLAndToken(urlUser: UserDefaultsRepository.url.value, token: UserDefaultsRepository.token.value) { error in
+            DispatchQueue.main.async {
+                self.updateStatusLabel(error: error)
+            }
+        }
+    }
 }

+ 74 - 0
LoopFollow/helpers/NightscoutUtils.swift

@@ -0,0 +1,74 @@
+//
+//  NightscoutUtils.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2023-04-09.
+//  Copyright © 2023 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class NightscoutUtils {
+    enum NightscoutError {
+        case emptyAddress
+        case invalidURL
+        case networkError
+        case siteNotFound
+        case invalidToken
+        case tokenRequired
+        case unknown
+    }
+    
+    static func createURLRequest(url: String, token: String?, path: String) -> URLRequest? {
+        var requestURLString = "\(url)\(path)"
+        
+        if let token = token {
+            let encodedToken = token.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? token
+            requestURLString += "?token=\(encodedToken)"
+        }
+        
+        guard let requestURL = URL(string: requestURLString) else {
+            return nil
+        }
+        
+        var request = URLRequest(url: requestURL)
+        request.httpMethod = "GET"
+        return request
+    }
+    
+    static func verifyURLAndToken(urlUser: String, token: String?, completion: @escaping (NightscoutError?) -> Void) {
+        if urlUser.isEmpty {
+            completion(.emptyAddress)
+            return
+        }
+
+        guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status") else {
+            completion(.invalidURL)
+            return
+        }
+        
+        let task = URLSession.shared.dataTask(with: request) { data, response, error in
+            if let httpResponse = response as? HTTPURLResponse {
+                switch httpResponse.statusCode {
+                case 200:
+                    completion(nil)
+                case 401:
+                    if token == nil || token!.isEmpty {
+                        completion(.tokenRequired)
+                    } else {
+                        completion(.invalidToken) // Change this from "unauthorized"
+                    }
+                default:
+                    completion(.unknown)
+                }
+            }  else {
+                if let _ = error {
+                    completion(.siteNotFound)
+                } else {
+                    completion(.networkError)
+                }
+            }
+        }
+        task.resume()
+    }
+}

+ 4 - 3
LoopFollow/repository/UserDefaults.swift

@@ -29,9 +29,10 @@ class UserDefaultsRepository {
         "SAGE",
         "CAGE",
         "Rec. Bolus",
-        "Pred."])
-    static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: [0,1,2,3,4,5,6,7,8,9])
-    static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: [true,true,true,true,true,true,true,true,true,true])
+        "Pred.",
+        "Carbs today"])
+    static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: [0,1,2,3,4,5,6,7,8,9,10])
+    static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: [true,true,true,true,true,true,true,true,true,true,true])
     static let hideInfoTable = UserDefaultsValue<Bool>(key: "hideInfoTable", default: false)
     
     // Nightscout Settings