فهرست منبع

Pump history uploading

Ivan Valkou 5 سال پیش
والد
کامیت
cdc50c35f4

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -148,6 +148,7 @@
 		388E597225AD9CF10019842D /* json in Resources */ = {isa = PBXBuildFile; fileRef = 388E597125AD9CF10019842D /* json */; };
 		388E5A5C25B6F0770019842D /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E5A5B25B6F0770019842D /* JSON.swift */; };
 		388E5A6025B6F2310019842D /* Autosens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E5A5F25B6F2310019842D /* Autosens.swift */; };
+		389442CB25F65F7100FA1F27 /* NightscoutTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */; };
 		3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3895E4C525B9E00D00214B37 /* Preferences.swift */; };
 		389FE32725F3ABE6002E92E0 /* CareKitUI in Frameworks */ = {isa = PBXBuildFile; productRef = 389FE32625F3ABE6002E92E0 /* CareKitUI */; };
 		389FE32A25F3AC44002E92E0 /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */; };
@@ -410,6 +411,7 @@
 		388E597125AD9CF10019842D /* json */ = {isa = PBXFileReference; lastKnownFileType = folder; path = json; sourceTree = "<group>"; };
 		388E5A5B25B6F0770019842D /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
 		388E5A5F25B6F2310019842D /* Autosens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Autosens.swift; sourceTree = "<group>"; };
+		389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutTreatment.swift; sourceTree = "<group>"; };
 		3895E4C525B9E00D00214B37 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
 		389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartView.swift; sourceTree = "<group>"; };
 		38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorage.swift; sourceTree = "<group>"; };
@@ -989,6 +991,7 @@
 				38A0364125ED069400FCBB52 /* TempBasal.swift */,
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3811DE8E25C9D80400A708ED /* User.swift */,
+				389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1636,6 +1639,7 @@
 				1D086541F369D339A74893AC /* BasalProfileEditorBuilder.swift in Sources */,
 				385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */,
 				8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */,
+				389442CB25F65F7100FA1F27 /* NightscoutTreatment.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorViewModel.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,

+ 1 - 0
FreeAPS/Resources/json/defaults/nightscout/uploaded-pumphistory.json

@@ -0,0 +1 @@
+[]

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

@@ -67,6 +67,10 @@ extension OpenAPS {
         static let exportDefaults = "exportDefaults"
     }
 
+    enum Nightscout {
+        static let uploadedPumphistory = "upload/uploaded-pumphistory.json"
+    }
+
     enum FreeAPS {
         static let settings = "freeaps/freeaps_settings.json"
         static let announcements = "freeaps/announcements.json"

+ 79 - 2
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -3,14 +3,21 @@ import LoopKit
 import SwiftDate
 import Swinject
 
+protocol PumpHistoryObserver {
+    func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent])
+}
+
 protocol PumpHistoryStorage {
     func storePumpEvents(_ events: [NewPumpEvent])
     func storeJournalCarbs(_ carbs: Int)
+    func recent() -> [PumpHistoryEvent]
+    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
 }
 
 final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     private let processQueue = DispatchQueue(label: "BasePumpHistoryStorage.processQueue")
     @Injected() private var storage: FileStorage!
+    @Injected() private var broadcaster: Broadcaster!
 
     init(resolver: Resolver) {
         injectServices(resolver)
@@ -48,7 +55,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             amount: nil,
                             duration: nil,
                             durationMin: minutes,
-                            rate: rate,
+                            rate: nil,
                             temp: nil,
                             carbInput: nil
                         ),
@@ -151,12 +158,82 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     private func processNewEvents(_ events: [PumpHistoryEvent]) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         let file = OpenAPS.Monitor.pumpHistory
+        var uniqEvents: [PumpHistoryEvent] = []
         try? storage.transaction { storage in
             try storage.append(events, to: file, uniqBy: \.id)
-            let uniqEvents = try storage.retrieve(file, as: [PumpHistoryEvent].self)
+            uniqEvents = try storage.retrieve(file, as: [PumpHistoryEvent].self)
                 .filter { $0.timestamp.addingTimeInterval(1.days.timeInterval) > Date() }
                 .sorted { $0.timestamp > $1.timestamp }
             try storage.save(Array(uniqEvents), as: file)
         }
+        broadcaster.notify(PumpHistoryObserver.self, on: processQueue) {
+            $0.pumpHistoryDidUpdate(uniqEvents)
+        }
+    }
+
+    func recent() -> [PumpHistoryEvent] {
+        (try? storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self))?.reversed() ?? []
+    }
+
+    func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
+        let events = recent()
+        guard !events.isEmpty else { return [] }
+
+        let temps: [NigtscoutTreatment] = events.reduce([]) { result, event in
+            var result = result
+            switch event.type {
+            case .tempBasal:
+                result.append(NigtscoutTreatment(
+                    duration: nil,
+                    rawDuration: nil,
+                    rawRate: event,
+                    absolute: event.rate,
+                    rate: event.rate,
+                    eventType: .nsTempBasal,
+                    createdAt: event.timestamp,
+                    entededBy: NigtscoutTreatment.local,
+                    bolus: nil,
+                    insulin: nil,
+                    notes: nil,
+                    carbs: nil
+                ))
+            case .tempBasalDuration:
+                if var last = result.popLast(), last.eventType == .nsTempBasal, last.createdAt == event.timestamp {
+                    last.duration = event.durationMin
+                    last.rawDuration = event
+                    result.append(last)
+                }
+            default: break
+            }
+            return result
+        }
+
+        let boluses = events.compactMap { event -> NigtscoutTreatment? in
+            switch event.type {
+            case .bolus:
+                return NigtscoutTreatment(
+                    duration: event.duration,
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .bolus,
+                    createdAt: event.timestamp,
+                    entededBy: NigtscoutTreatment.local,
+                    bolus: event,
+                    insulin: event.amount,
+                    notes: nil,
+                    carbs: nil
+                )
+            default: return nil
+            }
+        }
+
+        let uploaded = (try? storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self)) ?? []
+
+        let treatments = Array(Set([boluses, temps].flatMap { $0 }).subtracting(Set(uploaded)))
+
+        return treatments.sorted { $0.createdAt! > $1.createdAt! }
+//            .filter { $0.createdAt!.addingTimeInterval(3.hours.timeInterval) > Date() }
     }
 }

+ 4 - 0
FreeAPS/Sources/Logger/Logger.swift

@@ -110,6 +110,7 @@ final class Logger {
     static let openAPS = Logger(category: .openAPS, reporter: baseReporter)
     static let deviceManager = Logger(category: .deviceManager, reporter: baseReporter)
     static let apsManager = Logger(category: .apsManager, reporter: baseReporter)
+    static let nightscout = Logger(category: .nightscout, reporter: baseReporter)
 
     enum Category: String {
         case `default`
@@ -118,6 +119,7 @@ final class Logger {
         case openAPS
         case deviceManager
         case apsManager
+        case nightscout
 
         var name: String {
             rawValue.capitalizingFirstLetter()
@@ -131,6 +133,7 @@ final class Logger {
             case .openAPS: return .openAPS
             case .deviceManager: return .deviceManager
             case .apsManager: return .apsManager
+            case .nightscout: return .nightscout
             }
         }
 
@@ -141,6 +144,7 @@ final class Logger {
             case .apsManager,
                  .businessLogic,
                  .deviceManager,
+                 .nightscout,
                  .openAPS,
                  .service:
                 return OSLog(subsystem: subsystem, category: name)

+ 45 - 0
FreeAPS/Sources/Models/NightscoutTreatment.swift

@@ -0,0 +1,45 @@
+import Foundation
+
+struct NigtscoutTreatment: JSON, Hashable, Equatable {
+    var duration: Int?
+    var rawDuration: PumpHistoryEvent?
+    var rawRate: PumpHistoryEvent?
+    var absolute: Decimal?
+    var rate: Decimal?
+    var eventType: PumpHistoryEventType
+    var createdAt: Date?
+    var entededBy: String?
+    var bolus: PumpHistoryEvent?
+    var insulin: Decimal?
+    var notes: String?
+    var carbs: Decimal?
+
+    static let local = "freeaps-x://local"
+
+    static let empty = NigtscoutTreatment(from: "{}")!
+
+    static func == (lhs: NigtscoutTreatment, rhs: NigtscoutTreatment) -> Bool {
+        (lhs.createdAt ?? Date()) == (rhs.createdAt ?? Date())
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(createdAt ?? Date())
+    }
+}
+
+extension NigtscoutTreatment {
+    private enum CodingKeys: String, CodingKey {
+        case duration
+        case rawDuration = "raw_duration"
+        case rawRate = "raw_rate"
+        case absolute
+        case rate
+        case eventType
+        case createdAt = "created_at"
+        case entededBy
+        case bolus
+        case insulin
+        case notes
+        case carbs
+    }
+}

+ 2 - 0
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -25,6 +25,8 @@ enum PumpHistoryEventType: String, JSON {
     case rewind = "Rewind"
     case prime = "Prime"
     case journalCarbs = "JournalEntryMealMarker"
+
+    case nsTempBasal = "Temp Basal"
 }
 
 enum TempType: String, JSON {

+ 29 - 4
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -11,8 +11,8 @@ class NightscoutAPI {
     private enum Config {
         static let entriesPath = "/api/v1/entries/sgv.json"
         static let treatmentsPath = "/api/v1/treatments.json"
-        static let retryCount = 2
-        static let timeout: TimeInterval = 2
+        static let retryCount = 1
+        static let timeout: TimeInterval = 30
     }
 
     enum Error: LocalizedError {
@@ -40,7 +40,7 @@ extension NightscoutAPI {
         if let secret = secret {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
-        request.httpBody = try! JSONEncoder().encode(check)
+        request.httpBody = try! JSONCoding.encoder.encode(check)
         return service.run(request)
             .map { _ in () }
             .eraseToAnyPublisher()
@@ -91,7 +91,8 @@ extension NightscoutAPI {
         components.path = Config.treatmentsPath
         components.queryItems = [
             URLQueryItem(name: "find[carbs][$exists]", value: "true"),
-            URLQueryItem(name: "find[enteredBy][$ne]", value: CarbsEntry.manual)
+            URLQueryItem(name: "find[enteredBy][$ne]", value: CarbsEntry.manual),
+            URLQueryItem(name: "find[enteredBy][$ne]", value: NigtscoutTreatment.local)
         ]
         if let date = sinceDate {
             let dateItem = URLQueryItem(
@@ -178,6 +179,30 @@ extension NightscoutAPI {
             .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
             .eraseToAnyPublisher()
     }
+
+    func uploadTreatments(_ treatments: [NigtscoutTreatment]) -> AnyPublisher<Void, Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+
+        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(treatments)
+        request.httpMethod = "POST"
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .map { _ in () }
+            .eraseToAnyPublisher()
+    }
 }
 
 private extension String {

+ 34 - 7
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -7,7 +7,6 @@ protocol NightscoutManager {
     func fetchCarbs() -> AnyPublisher<Void, Never>
     func fetchTempTargets() -> AnyPublisher<Void, Never>
     func fetchAnnouncements() -> AnyPublisher<Void, Never>
-    func upload()
 }
 
 final class BaseNightscoutManager: NightscoutManager, Injectable {
@@ -15,10 +14,15 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
+    @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
+    @Injected() private var storage: FileStorage!
     @Injected() private var announcementsStorage: AnnouncementsStorage!
+    @Injected() private var broadcaster: Broadcaster!
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
 
+    private var lifetime = Set<AnyCancellable>()
+
     private var nightscoutAPI: NightscoutAPI? {
         guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
               let url = URL(string: urlString),
@@ -31,6 +35,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
     init(resolver: Resolver) {
         injectServices(resolver)
+        subscribe()
+    }
+
+    private func subscribe() {
+        broadcaster.register(PumpHistoryObserver.self, observer: self)
     }
 
     func fetchGlucose() -> AnyPublisher<Void, Never> {
@@ -90,12 +99,30 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }.eraseToAnyPublisher()
     }
 
-    func upload() {
-        uploadStatus()
-        uploadTreatments()
-    }
-
     private func uploadStatus() {}
 
-    private func uploadTreatments() {}
+    private func uploadPumpHistory(_ treatments: [NigtscoutTreatment]) {
+        guard !treatments.isEmpty, let nightscout = nightscoutAPI else {
+            return
+        }
+
+        processQueue.async {
+            nightscout.uploadTreatments(treatments)
+                .sink { completion in
+                    switch completion {
+                    case .finished:
+                        try? self.storage.save(treatments, as: OpenAPS.Nightscout.uploadedPumphistory)
+                    case let .failure(error):
+                        debug(.nightscout, error.localizedDescription)
+                    }
+                } receiveValue: {}
+                .store(in: &self.lifetime)
+        }
+    }
+}
+
+extension BaseNightscoutManager: PumpHistoryObserver {
+    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
+        uploadPumpHistory(pumpHistoryStorage.nightscoutTretmentsNotUploaded())
+    }
 }