Przeglądaj źródła

Decouple IoB from Determination for the HomeView

Sam King 9 miesięcy temu
rodzic
commit
74e4670b0e

+ 26 - 14
Trio.xcodeproj/project.pbxproj

@@ -256,6 +256,7 @@
 		3BD6CE262DC24CFD00FA0472 /* pumphistory-24h-zoned.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */; };
 		3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687B2D8DDD4600899469 /* SlideButton */; };
 		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
+		3BF85FE32E427312000D7351 /* IOBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF85FE12E427312000D7351 /* IOBService.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
@@ -1074,6 +1075,7 @@
 		3BD6CE252DC24CFD00FA0472 /* pumphistory-24h-zoned.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-24h-zoned.json"; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
+		3BF85FE12E427312000D7351 /* IOBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBService.swift; sourceTree = "<group>"; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
 		3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = "<group>"; };
 		42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = "<group>"; };
@@ -2018,17 +2020,18 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
-				BD47FD112D88AA630043966B /* OnboardingManager */,
-				DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
-				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
 				3811DE9225C9D88200A708ED /* Appearance */,
+				DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
+				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
 				3862CC2C2743F9DC00BF832C /* Calendar */,
 				E592A37E2CEEC046009A472C /* ContactImage */,
 				F90692A8274B7A980037068D /* HealthKit */,
+				3BF85FE22E427312000D7351 /* IOB */,
 				6B1A8D2C2B156EC100E76752 /* LiveActivity */,
 				3811DE9425C9D88200A708ED /* Network */,
 				38B4F3C425E5016800E76A18 /* Notifications */,
+				BD47FD112D88AA630043966B /* OnboardingManager */,
 				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
 				38AEE75025F021F10013F05B /* SettingsManager */,
 				3811DE9825C9D88300A708ED /* Storage */,
@@ -2607,6 +2610,14 @@
 			path = JSONImporterData;
 			sourceTree = "<group>";
 		};
+		3BF85FE22E427312000D7351 /* IOB */ = {
+			isa = PBXGroup;
+			children = (
+				3BF85FE12E427312000D7351 /* IOBService.swift */,
+			);
+			path = IOB;
+			sourceTree = "<group>";
+		};
 		4E8C7B59F8065047ECE20965 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -2659,22 +2670,22 @@
 			isa = PBXGroup;
 			children = (
 				49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */,
-				581516A82BCEEDF800BF67D7 /* NSPredicates.swift */,
-				583684052BD178DB00070A60 /* GlucoseStored+helper.swift */,
-				58F107732BD1A4D000B1A680 /* Determination+helper.swift */,
 				5837A52F2BD2E3C700A5DC04 /* CarbEntryStored+helper.swift */,
-				585E2CAD2BE7BF46006ECF1A /* PumpEvent+helper.swift */,
-				CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */,
-				5887527B2BD986E1008B081D /* OpenAPSBattery.swift */,
-				581AC4382BE22ED10038760C /* JSONConverter.swift */,
-				BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */,
+				BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */,
 				582FAE422C05102C00D1C13F /* CoreDataError.swift */,
 				BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */,
-				BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */,
+				58F107732BD1A4D000B1A680 /* Determination+helper.swift */,
+				CC76E9502BD4812E008BEB61 /* Forecast+helper.swift */,
+				583684052BD178DB00070A60 /* GlucoseStored+helper.swift */,
+				581AC4382BE22ED10038760C /* JSONConverter.swift */,
+				581516A82BCEEDF800BF67D7 /* NSPredicates.swift */,
+				5887527B2BD986E1008B081D /* OpenAPSBattery.swift */,
 				BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */,
-				BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */,
-				58A3D5432C96DE11003F90FC /* TempTargetStored+Helper.swift */,
+				BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */,
+				585E2CAD2BE7BF46006ECF1A /* PumpEvent+helper.swift */,
 				BD793CB12CE8032E00D669AC /* TempTargetRunStored.swift */,
+				58A3D5432C96DE11003F90FC /* TempTargetStored+Helper.swift */,
+				BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -4322,6 +4333,7 @@
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
 				3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
 				190EBCC829FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift in Sources */,
+				3BF85FE32E427312000D7351 /* IOBService.swift in Sources */,
 				DDF847EA2C5DABAC0049BB3B /* WatchConfigGarminView.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
 				BD4E1A7A2D3681B700D21626 /* GlucoseTargetSetup.swift in Sources */,

+ 0 - 12
Trio/Resources/InfoPlist.xcstrings

@@ -457,18 +457,6 @@
         }
       }
     },
-    "NSCalendarsFullAccessUsageDescription" : {
-      "comment" : "Privacy - Calendars Full Access Usage Description",
-      "extractionState" : "extracted_with_value",
-      "localizations" : {
-        "en" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
-          }
-        }
-      }
-    },
     "NSCalendarsUsageDescription" : {
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",

+ 5 - 0
Trio/Sources/APS/APSManager.swift

@@ -30,6 +30,7 @@ protocol APSManager {
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus(_ callback: ((Bool, String) -> Void)?) async
+    var iobFileDidUpdate: PassthroughSubject<Void, Never> { get }
 }
 
 enum APSError: LocalizedError {
@@ -107,6 +108,7 @@ final class BaseAPSManager: APSManager, Injectable {
     let isLooping = CurrentValueSubject<Bool, Never>(false)
     let lastLoopDateSubject = PassthroughSubject<Date, Never>()
     let lastError = CurrentValueSubject<Error?, Never>(nil)
+    let iobFileDidUpdate = PassthroughSubject<Void, Never>()
 
     let bolusProgress = CurrentValueSubject<Decimal?, Never>(nil)
 
@@ -459,6 +461,7 @@ final class BaseAPSManager: APSManager, Injectable {
             _ = try await autosenseResult
             try await openAPS.createProfiles()
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
+            iobFileDidUpdate.send(())
 
             guard isValidGlucoseData else {
                 throw APSError.glucoseError(message: "Glucose validation failed")
@@ -474,6 +477,8 @@ final class BaseAPSManager: APSManager, Injectable {
                 }
             }
         } catch {
+            iobFileDidUpdate.send(())
+
             // if we have a glucose validation error we might still run
             // determineBasal to try to get IoB and CoB updates but we
             // know that it will fail, so the invalidGlucoseError always

+ 2 - 1
Trio/Sources/APS/CGM/GlucoseSimulatorSource.swift

@@ -210,7 +210,8 @@ class OscillatingGenerator: BloodGlucoseGenerator {
                 direction = .flat
             } else {
                 // Generate a new glucose value
-                glucose = generate(date: currentDate)
+                // glucose = generate(date: currentDate)
+                glucose = 400
                 direction = calculateDirection(at: currentDate)
                 lastGeneratedGlucose = glucose
             }

+ 1 - 0
Trio/Sources/Application/TrioApp.swift

@@ -84,6 +84,7 @@ extension Notification.Name {
         if #available(iOS 16.2, *) {
             _ = resolver.resolve(LiveActivityManager.self)!
         }
+        _ = resolver.resolve(IOBService.self)!
     }
 
     init() {

+ 1 - 0
Trio/Sources/Assemblies/ServiceAssembly.swift

@@ -28,5 +28,6 @@ final class ServiceAssembly: Assembly {
                 LiveActivityManager(resolver: r)
             }
         }
+        container.register(IOBService.self) { r in BaseIOBService(resolver: r) }
     }
 }

+ 0 - 3
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -8832,9 +8832,6 @@
         }
       }
     },
-    "%lld h" : {
-
-    },
     "%lld hr" : {
       "localizations" : {
         "bg" : {

+ 13 - 0
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -20,6 +20,7 @@ extension Home {
         @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         @ObservationIgnored @Injected() var bluetoothManager: BluetoothStateManager!
+        @ObservationIgnored @Injected() var iobService: IOBService!
 
         var cgmStateModel: CGMSettings.StateModel {
             CGMSettings.StateModel.shared
@@ -65,6 +66,7 @@ extension Home {
         var manualTempBasal = false
         var isSmoothingEnabled = false
         var maxIOB: Decimal = 0.0
+        var currentIOB: Decimal = 0.0
         var autosensMax: Decimal = 1.2
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
@@ -215,12 +217,23 @@ extension Home {
                     group.addTask {
                         self.setupTempTargetsRunStored()
                     }
+                    group.addTask {
+                        self.iobService.updateIOB()
+                    }
                 }
             }
         }
 
         // These combine subscribers are only necessary due to the batch inserts of glucose/FPUs which do not trigger a ManagedObjectContext change notification
         private func registerSubscribers() {
+            iobService.iobPublisher
+                .receive(on: DispatchQueue.main)
+                .sink { [weak self] _ in
+                    guard let self = self else { return }
+                    self.currentIOB = self.iobService.currentIOB ?? 0
+                }
+                .store(in: &subscriptions)
+
             glucoseStorage.updatePublisher
                 .receive(on: queue)
                 .sink { [weak self] _ in

+ 1 - 1
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -432,7 +432,7 @@ extension Home {
                     Text(
                         (
                             Formatter.decimalFormatterWithTwoFractionDigits
-                                .string(from: (state.enactedAndNonEnactedDeterminations.first?.iob ?? 0) as NSNumber) ?? "0"
+                                .string(from: state.currentIOB as NSNumber) ?? "0"
                         ) +
                             String(localized: " U", comment: "Insulin unit")
                     )

+ 106 - 0
Trio/Sources/Services/IOB/IOBService.swift

@@ -0,0 +1,106 @@
+import Combine
+import CoreData
+import Foundation
+import Swinject
+
+protocol IOBService {
+    var iobPublisher: AnyPublisher<Decimal?, Never> { get }
+    var currentIOB: Decimal? { get }
+    func updateIOB()
+}
+
+final class BaseIOBService: IOBService, Injectable {
+    @Injected() private var fileStorage: FileStorage!
+    @Injected() private var determinationStorage: DeterminationStorage!
+    @Injected() private var apsManager: APSManager!
+
+    private let iobSubject = CurrentValueSubject<Decimal?, Never>(nil)
+    var iobPublisher: AnyPublisher<Decimal?, Never> {
+        iobSubject.eraseToAnyPublisher()
+    }
+
+    var currentIOB: Decimal? {
+        lookupIOB()
+    }
+
+    private var subscriptions = Set<AnyCancellable>()
+    private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
+    private let queue = DispatchQueue(label: "BaseIOBService.queue", qos: .background)
+    private let context = CoreDataStack.shared.newTaskContext()
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        coreDataPublisher =
+            changedObjectsOnManagedObjectContextDidSavePublisher()
+                .receive(on: queue)
+                .share()
+                .eraseToAnyPublisher()
+        subscribe()
+    }
+
+    private func subscribe() {
+        // Trigger update when a new determination is available
+        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
+            print("IOB-FILE: Determination update")
+            self?.updateIOB()
+        }.store(in: &subscriptions)
+
+        // Trigger update when the iob file is updated
+        apsManager.iobFileDidUpdate
+            .sink { [weak self] _ in
+                print("IOB-FILE: apsManager update")
+                self?.updateIOB()
+            }
+            .store(in: &subscriptions)
+    }
+
+    private func fetchLatestDeterminationIOB() -> (iob: Decimal?, date: Date?) {
+        var iob: Decimal?
+        var date: Date?
+        context.performAndWait {
+            let request = OrefDetermination.fetchRequest() as NSFetchRequest<OrefDetermination>
+            request.sortDescriptors = [NSSortDescriptor(key: "deliverAt", ascending: false)]
+            request.fetchLimit = 1
+            if let determination = try? context.fetch(request).first {
+                iob = determination.iob as? Decimal
+                date = determination.deliverAt
+            }
+        }
+        return (iob, date)
+    }
+
+    func lookupIOB() -> Decimal? {
+        let iobFromFile = fileStorage.retrieve(OpenAPS.Monitor.iob, as: [IOBEntry].self)
+        let iobFromFileValue = iobFromFile?.first?.iob
+        let iobFromFileDate = iobFromFile?.first?.time
+
+        let (iobFromDetermination, iobFromDeterminationDate) = fetchLatestDeterminationIOB()
+
+        var mostRecentIOB: Decimal?
+
+        if let iobFromFileValue = iobFromFileValue, let iobFromFileDate = iobFromFileDate {
+            if let iobFromDetermination = iobFromDetermination, let iobFromDeterminationDate = iobFromDeterminationDate {
+                if iobFromFileDate > iobFromDeterminationDate {
+                    mostRecentIOB = iobFromFileValue
+                } else {
+                    mostRecentIOB = iobFromDetermination
+                }
+            } else {
+                mostRecentIOB = iobFromFileValue
+            }
+        } else {
+            mostRecentIOB = iobFromDetermination
+        }
+
+        return mostRecentIOB
+    }
+
+    func updateIOB() {
+        Task {
+            let mostRecentIOB = lookupIOB()
+            if iobSubject.value != mostRecentIOB {
+                iobSubject.send(mostRecentIOB)
+            }
+        }
+    }
+}