Jelajahi Sumber

Upload glucose to nightscout option

Ivan Valkou 4 tahun lalu
induk
melakukan
232abd5f03

+ 2 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -9,5 +9,6 @@
     "debugOptions": false,
     "insulinReqFraction": 0.7,
     "skipBolusScreenAfterCarbs": false,
-    "cgm": "nightscout"
+    "cgm": "nightscout",
+    "uploadGlucose": false
 }

+ 1 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -61,6 +61,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                     debug(.nightscout, "New glucose found")
                     self.glucoseStorage.storeGlucose(filtered)
                     self.apsManager.heartbeat(date: date, force: false)
+                    self.nightscoutManager.uploadGlucose()
                 }
             }
             .store(in: &lifetime)

+ 1 - 0
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -76,6 +76,7 @@ extension OpenAPS {
         static let uploadedPumphistory = "upload/uploaded-pumphistory.json"
         static let uploadedCarbs = "upload/uploaded-carbs.json"
         static let uploadedTempTargets = "upload/uploaded-temptargets.json"
+        static let uploadedGlucose = "upload/uploaded-glucose.json"
     }
 
     enum FreeAPS {

+ 8 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -10,6 +10,7 @@ protocol GlucoseStorage {
     func lastGlucoseDate() -> Date
     func isGlucoseFresh() -> Bool
     func isGlucoseNotFlat() -> Bool
+    func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
 }
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -92,6 +93,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 .uniqued()
         ).count != 1
     }
+
+    func nightscoutGlucoseNotUploaded() -> [BloodGlucose] {
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedGlucose, as: [BloodGlucose].self) ?? []
+        let recentGlucose = recent()
+
+        return Array(Set(recentGlucose).subtracting(Set(uploaded)))
+    }
 }
 
 protocol GlucoseObserver {

+ 8 - 0
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -31,6 +31,14 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
     var glucose: Int?
 
     var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 }
+
+    static func == (lhs: BloodGlucose, rhs: BloodGlucose) -> Bool {
+        lhs.dateString == rhs.dateString
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(dateString)
+    }
 }
 
 enum GlucoseUnits: String, JSON, Equatable {

+ 1 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -12,4 +12,5 @@ struct FreeAPSSettings: JSON, Equatable {
     var insulinReqFraction: Decimal?
     var skipBolusScreenAfterCarbs: Bool?
     var cgm: CGMType?
+    var uploadGlucose: Bool?
 }

+ 10 - 1
FreeAPS/Sources/Modules/CGM/CGMViewModel.swift

@@ -5,10 +5,12 @@ extension CGM {
         @Injected() var settingsManager: SettingsManager!
 
         @Published var cgm: CGMType = .nightscout
-        @Published var transmitterID: String = ""
+        @Published var transmitterID = ""
+        @Published var uploadGlucose = false
 
         override func subscribe() {
             cgm = settingsManager.settings.cgm ?? .nightscout
+            uploadGlucose = settingsManager.settings.uploadGlucose ?? false
             transmitterID = UserDefaults.standard.dexcomTransmitterID ?? ""
 
             $cgm
@@ -17,6 +19,13 @@ extension CGM {
                     self?.settingsManager.settings.cgm = value
                 }
                 .store(in: &lifetime)
+
+            $uploadGlucose
+                .removeDuplicates()
+                .sink { [weak self] value in
+                    self?.settingsManager.settings.uploadGlucose = value
+                }
+                .store(in: &lifetime)
         }
 
         func onChangeID() {

+ 4 - 0
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -24,6 +24,10 @@ extension CGM {
                             .keyboardType(.asciiCapable)
                     }
                 }
+
+                Section(header: Text("Other")) {
+                    Toggle("Upload glucose to Nightscout", isOn: $viewModel.uploadGlucose)
+                }
             }
             .navigationTitle("CGM")
             .navigationBarTitleDisplayMode(.automatic)

+ 25 - 0
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -10,6 +10,7 @@ class NightscoutAPI {
 
     private enum Config {
         static let entriesPath = "/api/v1/entries/sgv.json"
+        static let uploadEntriesPath = "/api/v1/entries.json"
         static let treatmentsPath = "/api/v1/treatments.json"
         static let statusPath = "/api/v1/devicestatus.json"
         static let retryCount = 1
@@ -267,6 +268,30 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
+    func uploadGlucose(_ glucose: [BloodGlucose]) -> AnyPublisher<Void, Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.uploadEntriesPath
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+        request.httpBody = try! JSONCoding.encoder.encode(glucose)
+        request.httpMethod = "POST"
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .map { _ in () }
+            .eraseToAnyPublisher()
+    }
+
     func uploadStatus(_ status: NightscoutStatus) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         components.scheme = url.scheme

+ 38 - 0
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -10,6 +10,7 @@ protocol NightscoutManager: GlucoseSource {
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
     func deleteCarbs(at date: Date)
     func uploadStatus()
+    func uploadGlucose()
     var cgmURL: URL? { get }
 }
 
@@ -37,6 +38,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         settingsManager.settings.isUploadEnabled ?? false
     }
 
+    private var isUploadGlucoseEnabled: Bool {
+        settingsManager.settings.uploadGlucose ?? false
+    }
+
     private var nightscoutAPI: NightscoutAPI? {
         guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
               let url = URL(string: urlString),
@@ -221,6 +226,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
+    func uploadGlucose() {
+        uploadGlucose(glucoseStorage.nightscoutGlucoseNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedGlucose)
+    }
+
     private func uploadPumpHistory() {
         uploadTreatments(pumpHistoryStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedPumphistory)
     }
@@ -233,6 +242,35 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         uploadTreatments(tempTargetsStorage.nightscoutTretmentsNotUploaded(), fileToSave: OpenAPS.Nightscout.uploadedTempTargets)
     }
 
+    private func uploadGlucose(_ glucose: [BloodGlucose], fileToSave: String) {
+        guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
+            return
+        }
+
+        processQueue.async {
+            glucose.chunks(ofCount: 100)
+                .map { chunk -> AnyPublisher<Void, Error> in
+                    nightscout.uploadGlucose(Array(chunk))
+                }
+                .reduce(
+                    Just(()).setFailureType(to: Error.self)
+                        .eraseToAnyPublisher()
+                ) { (result, next) -> AnyPublisher<Void, Error> in
+                    Publishers.Concatenate(prefix: result, suffix: next).eraseToAnyPublisher()
+                }
+                .dropFirst()
+                .sink { completion in
+                    switch completion {
+                    case .finished:
+                        self.storage.save(glucose, as: fileToSave)
+                    case let .failure(error):
+                        debug(.nightscout, error.localizedDescription)
+                    }
+                } receiveValue: {}
+                .store(in: &self.lifetime)
+        }
+    }
+
     private func uploadTreatments(_ treatments: [NigtscoutTreatment], fileToSave: String) {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
             return