Procházet zdrojové kódy

Fetch carbs & temp targets from NS

Ivan Valkou před 5 roky
rodič
revize
7754dbea51

+ 12 - 8
FreeAPS.xcodeproj/project.pbxproj

@@ -62,7 +62,7 @@
 		3811DE8F25C9D80400A708ED /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE8E25C9D80400A708ED /* User.swift */; };
 		3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE9325C9D88200A708ED /* AppearanceManager.swift */; };
 		3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE9625C9D88300A708ED /* HTTPResponseStatus.swift */; };
-		3811DEAC25C9D88300A708ED /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE9725C9D88300A708ED /* NetworkManager.swift */; };
+		3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE9725C9D88300A708ED /* NightscoutManager.swift */; };
 		3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE9A25C9D88300A708ED /* UserDefaults+Cache.swift */; };
 		3811DEAE25C9D88300A708ED /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE9B25C9D88300A708ED /* Cache.swift */; };
 		3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE9C25C9D88300A708ED /* KeyValueStorage.swift */; };
@@ -122,6 +122,7 @@
 		38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A9260425F012D8009E3739 /* CarbRatios.swift */; };
 		38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */; };
 		38AEE75225F022080013F05B /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75125F022080013F05B /* SettingsManager.swift */; };
+		38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75625F0F18E0013F05B /* CarbsStorage.swift */; };
 		38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 38B17B6525DD90E0005CAE3D /* SwiftDate */; };
 		38B17B8625DD93BA005CAE3D /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17AD125DD6A40005CAE3D /* LoopKit.framework */; };
 		38B17B8725DD93BA005CAE3D /* LoopKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17AD125DD6A40005CAE3D /* LoopKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -159,7 +160,7 @@
 		38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */; };
 		38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C4D33925E9A1ED00D30B77 /* NSObject+AssociatedValues.swift */; };
 		38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D0B3B525EBE24900CB6E88 /* Battery.swift */; };
-		38D0B3D925EC07C400CB6E88 /* CarbHystoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D0B3D825EC07C400CB6E88 /* CarbHystoryEntry.swift */; };
+		38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */; };
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */; };
@@ -641,7 +642,7 @@
 		3811DE8E25C9D80400A708ED /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
 		3811DE9325C9D88200A708ED /* AppearanceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppearanceManager.swift; sourceTree = "<group>"; };
 		3811DE9625C9D88300A708ED /* HTTPResponseStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPResponseStatus.swift; sourceTree = "<group>"; };
-		3811DE9725C9D88300A708ED /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
+		3811DE9725C9D88300A708ED /* NightscoutManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutManager.swift; sourceTree = "<group>"; };
 		3811DE9A25C9D88300A708ED /* UserDefaults+Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Cache.swift"; sourceTree = "<group>"; };
 		3811DE9B25C9D88300A708ED /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
 		3811DE9C25C9D88300A708ED /* KeyValueStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyValueStorage.swift; sourceTree = "<group>"; };
@@ -691,6 +692,7 @@
 		38A9260425F012D8009E3739 /* CarbRatios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatios.swift; sourceTree = "<group>"; };
 		38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeAPSSettings.swift; sourceTree = "<group>"; };
 		38AEE75125F022080013F05B /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
+		38AEE75625F0F18E0013F05B /* CarbsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorage.swift; sourceTree = "<group>"; };
 		38B17AAE25DD69FA005CAE3D /* SwiftCharts.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SwiftCharts.xcodeproj; path = SwiftCharts/SwiftCharts.xcodeproj; sourceTree = "<group>"; };
 		38B17AC025DD6A40005CAE3D /* LoopKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = LoopKit.xcodeproj; path = LoopKit/LoopKit.xcodeproj; sourceTree = "<group>"; };
 		38B17AF025DD6AE6005CAE3D /* MKRingProgressView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = MKRingProgressView.xcodeproj; path = MKRingProgressView/MKRingProgressView.xcodeproj; sourceTree = "<group>"; };
@@ -709,7 +711,7 @@
 		38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Extensions.swift"; sourceTree = "<group>"; };
 		38C4D33925E9A1ED00D30B77 /* NSObject+AssociatedValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+AssociatedValues.swift"; sourceTree = "<group>"; };
 		38D0B3B525EBE24900CB6E88 /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = "<group>"; };
-		38D0B3D825EC07C400CB6E88 /* CarbHystoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbHystoryEntry.swift; sourceTree = "<group>"; };
+		38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsEntry.swift; sourceTree = "<group>"; };
 		38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetsStorage.swift; sourceTree = "<group>"; };
 		38FCF3D525E8FDF40078B0D1 /* MD5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MD5.swift; sourceTree = "<group>"; };
 		38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FreeAPSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1048,7 +1050,7 @@
 			isa = PBXGroup;
 			children = (
 				3811DE9625C9D88300A708ED /* HTTPResponseStatus.swift */,
-				3811DE9725C9D88300A708ED /* NetworkManager.swift */,
+				3811DE9725C9D88300A708ED /* NightscoutManager.swift */,
 				38FE826925CC82DB001FF17A /* NetworkService.swift */,
 				38FE826C25CC8461001FF17A /* NightscoutAPI.swift */,
 			);
@@ -1213,7 +1215,7 @@
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
 				38D0B3B525EBE24900CB6E88 /* Battery.swift */,
 				3870FF4225EC13F40088248F /* BloodGlucose.swift */,
-				38D0B3D825EC07C400CB6E88 /* CarbHystoryEntry.swift */,
+				38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */,
 				38A9260425F012D8009E3739 /* CarbRatios.swift */,
 				3811DF0125CA9FEA00A708ED /* Credentials.swift */,
 				38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */,
@@ -1253,6 +1255,7 @@
 				38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
 				38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */,
 				38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */,
+				38AEE75625F0F18E0013F05B /* CarbsStorage.swift */,
 			);
 			path = Storage;
 			sourceTree = "<group>";
@@ -1934,6 +1937,7 @@
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				3811DE6B25C9D62600A708ED /* OnboardingProvider.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
+				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
 				3811DEC225C9D99900A708ED /* SecurityContainer.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
@@ -2000,10 +2004,10 @@
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
-				3811DEAC25C9D88300A708ED /* NetworkManager.swift in Sources */,
+				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				3811DE3325C9D49500A708ED /* HomeBuilder.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
-				38D0B3D925EC07C400CB6E88 /* CarbHystoryEntry.swift in Sources */,
+				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				3811DE2125C9D48300A708ED /* MainBuilder.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,

+ 6 - 7
FreeAPS/Sources/APS/APSManager.swift

@@ -17,8 +17,9 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
+    @Injected() private var carbsStorage: CarbsStorage!
     @Injected() private var deviceDataManager: DeviceDataManager!
-    @Injected() private var networkManager: NetworkManager!
+    @Injected() private var nightscout: NightscoutManager!
     @Injected() private var settingsManager: SettingsManager!
     private var openAPS: OpenAPS!
 
@@ -54,13 +55,11 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     func loop() {
-        loopCancellable = networkManager
+        loopCancellable = nightscout
             .fetchGlucose()
-            .flatMap { [weak self] glucose -> AnyPublisher<Bool, Never> in
-                guard let self = self else { return Just(false).eraseToAnyPublisher() }
-                self.glucoseStorage.storeGlucose(glucose)
-                return self.determineBasal()
-            }
+            .flatMap { self.nightscout.fetchCarbs() }
+            .flatMap { self.nightscout.fetchTempTargets() }
+            .flatMap { self.determineBasal() }
             .sink { _ in } receiveValue: { [weak self] ok in
                 guard let self = self else { return }
                 if ok, self.settings.closedLoop {

+ 39 - 0
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -0,0 +1,39 @@
+import Foundation
+import SwiftDate
+import Swinject
+
+protocol CarbsStorage {
+    func storeCarbs(_ carbs: [CarbsEntry])
+    func syncDate() -> Date
+}
+
+final class BaseCarbsStorage: CarbsStorage, Injectable {
+    private let processQueue = DispatchQueue(label: "BaseCarbsStorage.processQueue")
+    @Injected() private var storage: FileStorage!
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+    }
+
+    func storeCarbs(_ carbs: [CarbsEntry]) {
+        processQueue.sync {
+            let file = OpenAPS.Monitor.carbHistory
+            try? self.storage.transaction { storage in
+                try storage.append(carbs, to: file, uniqBy: \.createdAt)
+                let uniqEvents = try storage.retrieve(file, as: [CarbsEntry].self)
+                    .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
+                    .sorted { $0.createdAt > $1.createdAt }
+                try storage.save(Array(uniqEvents), as: file)
+            }
+        }
+    }
+
+    func syncDate() -> Date {
+        guard let events = try? storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self),
+              let recent = events.first
+        else {
+            return Date().addingTimeInterval(-1.days.timeInterval)
+        }
+        return recent.createdAt.addingTimeInterval(-6.minutes.timeInterval)
+    }
+}

+ 11 - 1
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -4,6 +4,7 @@ import Swinject
 
 protocol GlucoseStorage {
     func storeGlucose(_ glucose: [BloodGlucose])
+    func syncDate() -> Date
 }
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -15,7 +16,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     func storeGlucose(_ glucose: [BloodGlucose]) {
-        processQueue.async {
+        processQueue.sync {
             let file = OpenAPS.Monitor.glucose
             try? self.storage.transaction { storage in
                 try storage.append(glucose, to: file, uniqBy: \.dateString)
@@ -26,4 +27,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             }
         }
     }
+
+    func syncDate() -> Date {
+        guard let events = try? storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self),
+              let recent = events.first
+        else {
+            return Date().addingTimeInterval(-1.days.timeInterval)
+        }
+        return recent.dateString.addingTimeInterval(-6.minutes.timeInterval)
+    }
 }

+ 11 - 1
FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift

@@ -4,6 +4,7 @@ import Swinject
 
 protocol TempTargetsStorage {
     func storeTempTargets(_ targets: [TempTarget])
+    func syncDate() -> Date
 }
 
 final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
@@ -15,7 +16,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     }
 
     func storeTempTargets(_ targets: [TempTarget]) {
-        processQueue.async {
+        processQueue.sync {
             let file = OpenAPS.Settings.tempTargets
             try? self.storage.transaction { storage in
                 try storage.append(targets, to: file, uniqBy: \.createdAt)
@@ -26,4 +27,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             }
         }
     }
+
+    func syncDate() -> Date {
+        guard let events = try? storage.retrieve(OpenAPS.Settings.tempTargets, as: [TempTarget].self),
+              let recent = events.first
+        else {
+            return Date().addingTimeInterval(-1.days.timeInterval)
+        }
+        return recent.createdAt.addingTimeInterval(-6.minutes.timeInterval)
+    }
 }

+ 1 - 1
FreeAPS/Sources/Containers/NetworkContainer.swift

@@ -5,7 +5,7 @@ private let resolver = FreeAPSApp.resolver
 
 enum NetworkContainer: DependeciesContainer {
     static func register(container: Container) {
-        container.register(NetworkManager.self) { _ in BaseNetworkManager(resolver: resolver) }
+        container.register(NightscoutManager.self) { _ in BaseNightscoutManager(resolver: resolver) }
         container.register(AuthorizationManager.self) { _ in BaseAuthorizationManager(resolver: resolver) }
     }
 }

+ 1 - 0
FreeAPS/Sources/Containers/StorageContainer.swift

@@ -12,6 +12,7 @@ enum StorageContainer: DependeciesContainer {
         container.register(PumpHistoryStorage.self) { _ in BasePumpHistoryStorage(resolver: resolver) }
         container.register(GlucoseStorage.self) { _ in BaseGlucoseStorage(resolver: resolver) }
         container.register(TempTargetsStorage.self) { _ in BaseTempTargetsStorage(resolver: resolver) }
+        container.register(CarbsStorage.self) { _ in BaseCarbsStorage(resolver: resolver) }
         container.register(SettingsManager.self) { _ in BaseFSettingsManager(resolver: resolver) }
 
         container.register(Keychain.self) { _ in BaseKeychain() }

+ 2 - 15
FreeAPS/Sources/Helpers/JSON.swift

@@ -5,27 +5,14 @@ import Foundation
     init?(from: String)
 }
 
-private func encoder() -> JSONEncoder {
-    let encoder = JSONEncoder()
-    encoder.outputFormatting = .prettyPrinted
-    encoder.dateEncodingStrategy = .iso8601
-    return encoder
-}
-
-private func decoder() -> JSONDecoder {
-    let decoder = JSONDecoder()
-    decoder.dateDecodingStrategy = .iso8601
-    return decoder
-}
-
 extension JSON {
     var rawJSON: RawJSON {
-        String(data: try! encoder().encode(self), encoding: .utf8)!
+        String(data: try! JSONCoding.encoder.encode(self), encoding: .utf8)!
     }
 
     init?(from: String) {
         guard let data = from.data(using: .utf8),
-              let object = try? decoder().decode(Self.self, from: data)
+              let object = try? JSONCoding.decoder.decode(Self.self, from: data)
         else {
             return nil
         }

+ 2 - 2
FreeAPS/Sources/Models/CarbHystoryEntry.swift

@@ -1,12 +1,12 @@
 import Foundation
 
-struct CarbHystoryEntry: JSON {
+struct CarbsEntry: JSON {
     let createdAt: Date
     let carbs: Int
     let enteredBy: String?
 }
 
-extension CarbHystoryEntry {
+extension CarbsEntry {
     private enum CodingKeys: String, CodingKey {
         case createdAt = "created_at"
         case carbs

+ 1 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -50,6 +50,7 @@ extension Settings {
                             .modal(for: .configEditor(file: OpenAPS.Settings.pumpProfile), from: self)
                         Text("Profile").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.profile), from: self)
                         Text("Glucose").chevronCell().modal(for: .configEditor(file: OpenAPS.Monitor.glucose), from: self)
+                        Text("Carbs").chevronCell().modal(for: .configEditor(file: OpenAPS.Monitor.carbHistory), from: self)
                         Text("Suggested").chevronCell()
                             .modal(for: .configEditor(file: OpenAPS.Enact.suggested), from: self)
                         Text("Enacted").chevronCell()

+ 0 - 34
FreeAPS/Sources/Services/Network/NetworkManager.swift

@@ -1,34 +0,0 @@
-import Combine
-import Foundation
-import Swinject
-
-protocol NetworkManager {
-    func fetchGlucose() -> AnyPublisher<[BloodGlucose], Error>
-}
-
-final class BaseNetworkManager: NetworkManager, Injectable {
-    @Injected() private var keychain: Keychain!
-
-    private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
-
-    private var nightscoutAPI: NightscoutAPI? {
-        guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
-              let url = URL(string: urlString),
-              let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
-        else {
-            return nil
-        }
-        return NightscoutAPI(url: url, secret: secret)
-    }
-
-    init(resolver: Resolver) {
-        injectServices(resolver)
-    }
-
-    func fetchGlucose() -> AnyPublisher<[BloodGlucose], Error> {
-        guard let nightscout = nightscoutAPI else {
-            return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
-        }
-        return nightscout.fetchLast(288)
-    }
-}

+ 75 - 10
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -10,7 +10,9 @@ 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
     }
 
     enum Error: LocalizedError {
@@ -32,7 +34,7 @@ extension NightscoutAPI {
             var notes = "FreeAPS X connected"
         }
         let check = Check()
-        var request = URLRequest(url: url.appendingPathComponent("api/v1/treatments.json"))
+        var request = URLRequest(url: url.appendingPathComponent(Config.treatmentsPath))
         request.httpMethod = "POST"
         request.addValue("application/json", forHTTPHeaderField: "Content-Type")
         if let secret = secret {
@@ -44,26 +46,31 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
-    func fetchLast(_ count: Int) -> AnyPublisher<[BloodGlucose], Swift.Error> {
+    func fetchLastGlucose(_ count: Int, sinceDate: Date? = nil) -> AnyPublisher<[BloodGlucose], Swift.Error> {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.entriesPath
         components.queryItems = [URLQueryItem(name: "count", value: "\(count)")]
+        if let date = sinceDate {
+            let dateItem = URLQueryItem(
+                name: "find[dateString][$gte]",
+                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+            )
+            components.queryItems?.append(dateItem)
+        }
 
         var request = URLRequest(url: components.url!)
         request.allowsConstrainedNetworkAccess = false
-        request.timeoutInterval = 10
+        request.timeoutInterval = Config.timeout
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
 
-        return URLSession.shared.dataTaskPublisher(for: request)
+        return service.run(request)
             .retry(Config.retryCount)
-            .tryMap { output in
-                guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
-                    throw Error.badStatusCode
-                }
-                return output.data
-            }
             .decode(type: [BloodGlucose].self, decoder: JSONCoding.decoder)
             .map {
                 $0.filter { $0.isStateValid }
@@ -75,6 +82,64 @@ extension NightscoutAPI {
             }
             .eraseToAnyPublisher()
     }
+
+    func fetchCarbs(sinceDate: Date? = nil) -> AnyPublisher<[CarbsEntry], Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+        components.queryItems = [URLQueryItem(name: "find[carbs][$exists]", value: "true")]
+        if let date = sinceDate {
+            let dateItem = URLQueryItem(
+                name: "find[created_at][$gte]",
+                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+            )
+            components.queryItems?.append(dateItem)
+        }
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .decode(type: [CarbsEntry].self, decoder: JSONCoding.decoder)
+            .eraseToAnyPublisher()
+    }
+
+    func fetchTempTargets(sinceDate: Date? = nil) -> AnyPublisher<[TempTarget], Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+        components.queryItems = [URLQueryItem(name: "find[eventType]", value: "Temporary+Target")]
+        if let date = sinceDate {
+            let dateItem = URLQueryItem(
+                name: "find[created_at][$gte]",
+                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+            )
+            components.queryItems?.append(dateItem)
+        }
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
+            .eraseToAnyPublisher()
+    }
 }
 
 private extension String {

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

@@ -0,0 +1,75 @@
+import Combine
+import Foundation
+import Swinject
+
+protocol NightscoutManager {
+    func fetchGlucose() -> AnyPublisher<Void, Never>
+    func fetchCarbs() -> AnyPublisher<Void, Never>
+    func fetchTempTargets() -> AnyPublisher<Void, Never>
+}
+
+final class BaseNightscoutManager: NightscoutManager, Injectable {
+    @Injected() private var keychain: Keychain!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var tempTargetsStorage: TempTargetsStorage!
+    @Injected() private var carbsStorage: CarbsStorage!
+
+    private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
+
+    private var nightscoutAPI: NightscoutAPI? {
+        guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
+              let url = URL(string: urlString),
+              let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
+        else {
+            return nil
+        }
+        return NightscoutAPI(url: url, secret: secret)
+    }
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+    }
+
+    func fetchGlucose() -> AnyPublisher<Void, Never> {
+        guard let nightscout = nightscoutAPI else {
+            return Just(()).eraseToAnyPublisher()
+        }
+
+        let since = glucoseStorage.syncDate()
+        return nightscout.fetchLastGlucose(288, sinceDate: since)
+            .replaceError(with: [])
+            .map {
+                self.glucoseStorage.storeGlucose($0)
+                return ()
+            }
+            .eraseToAnyPublisher()
+    }
+
+    func fetchCarbs() -> AnyPublisher<Void, Never> {
+        guard let nightscout = nightscoutAPI else {
+            return Just(()).eraseToAnyPublisher()
+        }
+
+        let since = carbsStorage.syncDate()
+        return nightscout.fetchCarbs(sinceDate: since)
+            .replaceError(with: [])
+            .map {
+                self.carbsStorage.storeCarbs($0)
+                return ()
+            }.eraseToAnyPublisher()
+    }
+
+    func fetchTempTargets() -> AnyPublisher<Void, Never> {
+        guard let nightscout = nightscoutAPI else {
+            return Just(()).eraseToAnyPublisher()
+        }
+
+        let since = tempTargetsStorage.syncDate()
+        return nightscout.fetchTempTargets(sinceDate: since)
+            .replaceError(with: [])
+            .map {
+                self.tempTargetsStorage.storeTempTargets($0)
+                return ()
+            }.eraseToAnyPublisher()
+    }
+}