From f781fec66e06648d6d30d431c8ce2effee4e2dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=20M=C3=A5rtensson?= <53905247+Jon-b-m@users.noreply.github.com> Date: Sat, 11 Nov 2023 17:00:20 +0100 Subject: [PATCH] Bolus from watch (#326) Use same calculator on Watch as on iPhone app. --- .../Core_Data.xcdatamodel/contents | 1 + FreeAPS.xcodeproj/project.pbxproj | 4 + .../Sources/APS/Storage/CoreDataStorage.swift | 34 ++++++ .../Sources/APS/Storage/GlucoseStorage.swift | 3 + FreeAPS/Sources/Models/DateFilter.swift | 1 + .../Sources/Modules/Bolus/BolusProvider.swift | 16 +-- .../Modules/Bolus/BolusStateModel.swift | 5 +- .../Services/WatchManager/WatchManager.swift | 115 +++++++++++++----- .../DataFlow.swift | 1 + .../WatchStateModel.swift | 2 + 10 files changed, 137 insertions(+), 45 deletions(-) create mode 100644 FreeAPS/Sources/APS/Storage/CoreDataStorage.swift diff --git a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents index 3a617a7a32..372848b776 100644 --- a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents +++ b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents @@ -129,6 +129,7 @@ + diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index ebd4e222d9..aaac0746ad 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; }; 1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; }; 193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; }; + 1956FB212AFF79E200C7B4FF /* CoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */; }; 195D80B42AF6973A00D25097 /* DynamicRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B32AF6973A00D25097 /* DynamicRootView.swift */; }; 195D80B72AF697B800D25097 /* DynamicDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B62AF697B800D25097 /* DynamicDataFlow.swift */; }; 195D80B92AF697F700D25097 /* DynamicProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B82AF697F700D25097 /* DynamicProvider.swift */; }; @@ -532,6 +533,7 @@ 1927C8FE274489BA00347C69 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = ""; }; 1935363F28496F7D001E0B16 /* Oref2_variables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Oref2_variables.swift; sourceTree = ""; }; 193F6CDC2A512C8F001240FD /* Loops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loops.swift; sourceTree = ""; }; + 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStorage.swift; sourceTree = ""; }; 195D80B32AF6973A00D25097 /* DynamicRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRootView.swift; sourceTree = ""; }; 195D80B62AF697B800D25097 /* DynamicDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDataFlow.swift; sourceTree = ""; }; 195D80B82AF697F700D25097 /* DynamicProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicProvider.swift; sourceTree = ""; }; @@ -1706,6 +1708,7 @@ 38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */, 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */, CE82E02428E867BA00473A9C /* AlertStorage.swift */, + 1956FB202AFF79E200C7B4FF /* CoreDataStorage.swift */, ); path = Storage; sourceTree = ""; @@ -2905,6 +2908,7 @@ E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */, BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */, E3A08AAE59538BC8A8ABE477 /* NotificationsConfigDataFlow.swift in Sources */, + 1956FB212AFF79E200C7B4FF /* CoreDataStorage.swift in Sources */, 0F7A65FBD2CD8D6477ED4539 /* NotificationsConfigProvider.swift in Sources */, 3171D2818C7C72CD1584BB5E /* NotificationsConfigStateModel.swift in Sources */, CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */, diff --git a/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift b/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift new file mode 100644 index 0000000000..c0caa2fd7d --- /dev/null +++ b/FreeAPS/Sources/APS/Storage/CoreDataStorage.swift @@ -0,0 +1,34 @@ +import CoreData +import Foundation +import SwiftDate +import Swinject + +final class CoreDataStorage { + let coredataContext = CoreDataStack.shared.persistentContainer.viewContext // newBackgroundContext() + + func fetchGlucose(interval: NSDate) -> [Readings] { + var fetchGlucose = [Readings]() + coredataContext.performAndWait { + let requestReadings = Readings.fetchRequest() as NSFetchRequest + let sort = NSSortDescriptor(key: "date", ascending: false) + requestReadings.sortDescriptors = [sort] + requestReadings.predicate = NSPredicate( + format: "glucose > 0 AND date > %@", interval + ) + try? fetchGlucose = self.coredataContext.fetch(requestReadings) + } + return fetchGlucose + } + + func fetchLatestOverride() -> [Override] { + var overrideArray = [Override]() + coredataContext.performAndWait { + let requestOverrides = Override.fetchRequest() as NSFetchRequest + let sortOverride = NSSortDescriptor(key: "date", ascending: false) + requestOverrides.sortDescriptors = [sortOverride] + requestOverrides.fetchLimit = 1 + try? overrideArray = self.coredataContext.fetch(requestOverrides) + } + return overrideArray + } +} diff --git a/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift b/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift index d731e9fd56..838eed0821 100644 --- a/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift +++ b/FreeAPS/Sources/APS/Storage/GlucoseStorage.swift @@ -71,11 +71,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable { var bg_ = 0 var bgDate = Date() var id = "" + var direction = "" if glucose.isNotEmpty { bg_ = glucose[0].glucose ?? 0 bgDate = glucose[0].dateString id = glucose[0].id + direction = glucose[0].direction?.symbol ?? "↔︎" } if bg_ != 0 { @@ -84,6 +86,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable { dataForForStats.date = bgDate dataForForStats.glucose = Int16(bg_) dataForForStats.id = id + dataForForStats.direction = direction try? self.coredataContext.save() } } diff --git a/FreeAPS/Sources/Models/DateFilter.swift b/FreeAPS/Sources/Models/DateFilter.swift index 42a8b7dbe8..466b699f98 100644 --- a/FreeAPS/Sources/Models/DateFilter.swift +++ b/FreeAPS/Sources/Models/DateFilter.swift @@ -2,6 +2,7 @@ import Foundation struct DateFilter { + var twoHours = Date().addingTimeInterval(-2.hours.timeInterval) as NSDate var today = Calendar.current.startOfDay(for: Date()) as NSDate var day = Date().addingTimeInterval(-24.hours.timeInterval) as NSDate var week = Date().addingTimeInterval(-7.days.timeInterval) as NSDate diff --git a/FreeAPS/Sources/Modules/Bolus/BolusProvider.swift b/FreeAPS/Sources/Modules/Bolus/BolusProvider.swift index 962fd47f1d..a4bda5bfce 100644 --- a/FreeAPS/Sources/Modules/Bolus/BolusProvider.swift +++ b/FreeAPS/Sources/Modules/Bolus/BolusProvider.swift @@ -1,8 +1,6 @@ -import CoreData - extension Bolus { final class Provider: BaseProvider, BolusProvider { - let coredataContext = CoreDataStack.shared.persistentContainer.viewContext + let coreDataStorage = CoreDataStorage() var suggestion: Suggestion? { storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) @@ -15,17 +13,7 @@ extension Bolus { } func fetchGlucose() -> [Readings] { - var fetchGlucose = [Readings]() - coredataContext.performAndWait { - let requestReadings = Readings.fetchRequest() as NSFetchRequest - let sort = NSSortDescriptor(key: "date", ascending: true) - requestReadings.sortDescriptors = [sort] - requestReadings.predicate = NSPredicate( - format: "glucose > 0 AND date > %@", - Date().addingTimeInterval(-1.hours.timeInterval) as NSDate - ) - try? fetchGlucose = self.coredataContext.fetch(requestReadings) - } + let fetchGlucose = coreDataStorage.fetchGlucose(interval: DateFilter().twoHours) return fetchGlucose } } diff --git a/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift b/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift index cc64987ee4..ecbc2a28bd 100644 --- a/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift +++ b/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift @@ -100,15 +100,14 @@ extension Bolus { func getDeltaBG() { let glucose = provider.fetchGlucose() guard glucose.count >= 3 else { return } - let lastGlucose = glucose.last?.glucose ?? 0 - let thirdLastGlucose = glucose[glucose.count - 3] + let lastGlucose = glucose.first?.glucose ?? 0 + let thirdLastGlucose = glucose[2] let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose.glucose) deltaBG = delta } // CALCULATIONS FOR THE BOLUS CALCULATOR func calculateInsulin() -> Decimal { - // for mmol conversion var conversion: Decimal = 1.0 if units == .mmolL { conversion = 0.0555 diff --git a/FreeAPS/Sources/Services/WatchManager/WatchManager.swift b/FreeAPS/Sources/Services/WatchManager/WatchManager.swift index 16df25d2b4..e4d3b6d53d 100644 --- a/FreeAPS/Sources/Services/WatchManager/WatchManager.swift +++ b/FreeAPS/Sources/Services/WatchManager/WatchManager.swift @@ -1,4 +1,3 @@ -import CoreData import Foundation import Swinject import WatchConnectivity @@ -12,14 +11,13 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable { @Injected() private var broadcaster: Broadcaster! @Injected() private var settingsManager: SettingsManager! - @Injected() private var glucoseStorage: GlucoseStorage! @Injected() private var apsManager: APSManager! @Injected() private var storage: FileStorage! @Injected() private var carbsStorage: CarbsStorage! @Injected() private var tempTargetsStorage: TempTargetsStorage! @Injected() private var garmin: GarminManager! - let coredataContext = CoreDataStack.shared.persistentContainer.viewContext // newBackgroundContext() + let coreDataStorage = CoreDataStorage() private var lifetime = Lifetime() @@ -57,12 +55,13 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable { private func configureState() { processQueue.async { - let glucoseValues = self.glucoseText() + let readings = self.coreDataStorage.fetchGlucose(interval: DateFilter().twoHours) + let glucoseValues = self.glucoseText(readings) self.state.glucose = glucoseValues.glucose self.state.trend = glucoseValues.trend self.state.delta = glucoseValues.delta - self.state.trendRaw = self.glucoseStorage.recent().last?.direction?.rawValue - self.state.glucoseDate = self.glucoseStorage.recent().last?.dateString + self.state.trendRaw = readings.first?.direction ?? "↔︎" + self.state.glucoseDate = readings.first?.date ?? .distantPast self.state.glucoseDateInterval = self.state.glucoseDate.map { UInt64($0.timeIntervalSince1970) } self.state.lastLoopDate = self.enactedSuggestion?.recieved == true ? self.enactedSuggestion?.deliverAt : self .apsManager.lastLoopDate @@ -76,16 +75,26 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable { self.state.carbsRequired = self.suggestion?.carbsReq var insulinRequired = self.suggestion?.insulinReq ?? 0 + var double: Decimal = 2 - if (self.suggestion?.cob ?? 0) > 0 { - if self.suggestion?.manualBolusErrorString == 0 { - insulinRequired = self.suggestion?.insulinForManualBolus ?? 0 - double = 1 - } + if self.suggestion?.manualBolusErrorString == 0 { + insulinRequired = self.suggestion?.insulinForManualBolus ?? 0 + double = 1 } - self.state.bolusRecommended = self.apsManager - .roundBolus(amount: max(insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * double, 0)) + self.state.useNewCalc = self.settingsManager.settings.useCalc + + if !(self.state.useNewCalc ?? false) { + self.state.bolusRecommended = self.apsManager + .roundBolus(amount: max( + insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * double, + 0 + )) + } else { + let recommended = self.newBolusCalc(delta: readings, suggestion: self.suggestion) + self.state.bolusRecommended = self.apsManager + .roundBolus(amount: max(recommended, 0)) + } self.state.iob = self.suggestion?.iob self.state.cob = self.suggestion?.cob @@ -113,12 +122,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable { self.state.isf = self.suggestion?.isf - var overrideArray = [Override]() - let requestOverrides = Override.fetchRequest() as NSFetchRequest - let sortOverride = NSSortDescriptor(key: "date", ascending: false) - requestOverrides.sortDescriptors = [sortOverride] - requestOverrides.fetchLimit = 1 - try? overrideArray = self.coredataContext.fetch(requestOverrides) + let overrideArray = self.coreDataStorage.fetchLatestOverride() if overrideArray.first?.enabled ?? false { let percentString = "\((overrideArray.first?.percentage ?? 100).formatted(.number)) %" @@ -147,26 +151,25 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable { } } - private func glucoseText() -> (glucose: String, trend: String, delta: String) { - let glucose = glucoseStorage.recent() + private func glucoseText(_ glucose: [Readings]) -> (glucose: String, trend: String, delta: String) { + let glucoseValue = glucose.first?.glucose ?? 0 - guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { return ("--", "--", "--") } + guard !glucose.isEmpty else { return ("--", "--", "--") } - let delta = glucose.count >= 2 ? glucoseValue - (glucose[glucose.count - 2].glucose ?? 0) : nil + let delta = glucose.count >= 2 ? glucoseValue - glucose[1].glucose : nil let units = settingsManager.settings.units let glucoseText = glucoseFormatter .string(from: Double( - units == .mmolL ? glucoseValue - .asMmolL : Decimal(glucoseValue) + units == .mmolL ? Decimal(glucoseValue).asMmolL : Decimal(glucoseValue) ) as NSNumber)! - let directionText = lastGlucose.direction?.symbol ?? "↔︎" + + let directionText = glucose.first?.direction ?? "↔︎" let deltaText = delta .map { self.deltaFormatter .string(from: Double( - units == .mmolL ? $0 - .asMmolL : Decimal($0) + units == .mmolL ? Decimal($0).asMmolL : Decimal($0) ) as NSNumber)! } ?? "--" @@ -200,6 +203,62 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable { )! } + private func newBolusCalc(delta: [Readings], suggestion _: Suggestion?) -> Decimal { + var conversion: Decimal = 1 + // Settings + if settingsManager.settings.units == .mmolL { + conversion = 0.0555 + } + let isf = state.isf ?? 0 + let target = suggestion?.current_target ?? 0 + let carbratio = suggestion?.carbRatio ?? 0 + let bg = delta.first?.glucose ?? 0 + let cob = state.cob ?? 0 + let iob = state.iob ?? 0 + let useFattyMealCorrectionFactor = settingsManager.settings.fattyMeals + let fattyMealFactor = settingsManager.settings.fattyMealFactor + let maxBolus = settingsManager.pumpSettings.maxBolus + var insulinCalculated: Decimal = 0 + // insulin needed for the current blood glucose + let targetDifference = (Decimal(bg) - target) * conversion + let targetDifferenceInsulin = targetDifference / isf + // more or less insulin because of bg trend in the last 15 minutes + var bgDelta: Int = 0 + if delta.count >= 3 { + bgDelta = Int((delta.first?.glucose ?? 0) - delta[2].glucose) + } + let fifteenMinInsulin = (Decimal(bgDelta) * conversion) / isf + // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB + let wholeCobInsulin = cob / carbratio + // determine how much the calculator reduces/ increases the bolus because of IOB + let iobInsulinReduction = (-1) * iob + // adding everything together + // add a calc for the case that no fifteenMinInsulin is available + var wholeCalc: Decimal = 0 + if bgDelta != 0 { + wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin) + } else { + // add (rare) case that no glucose value is available -> maybe display warning? + // if no bg is available, ?? sets its value to 0 + if bg == 0 { + wholeCalc = (iobInsulinReduction + wholeCobInsulin) + } else { + wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin) + } + } + // apply custom factor at the end of the calculations + let result = wholeCalc * settingsManager.settings.overrideFactor + // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView) + if useFattyMealCorrectionFactor { + insulinCalculated = result * fattyMealFactor + } else { + insulinCalculated = result + } + // Not 0 or over maxBolus + insulinCalculated = max(min(insulinCalculated, maxBolus), 0) + return insulinCalculated + } + private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal diff --git a/FreeAPSWatch WatchKit Extension/DataFlow.swift b/FreeAPSWatch WatchKit Extension/DataFlow.swift index cf4b81602d..d6f7848c13 100644 --- a/FreeAPSWatch WatchKit Extension/DataFlow.swift +++ b/FreeAPSWatch WatchKit Extension/DataFlow.swift @@ -22,6 +22,7 @@ struct WatchState: Codable { var eventualBGRaw: String? var displayOnWatch: AwConfig? var displayFatAndProteinOnWatch: Bool? + var useNewCalc: Bool? var isf: Decimal? var override: String? } diff --git a/FreeAPSWatch WatchKit Extension/WatchStateModel.swift b/FreeAPSWatch WatchKit Extension/WatchStateModel.swift index 8d5a7b20ba..387e0920be 100644 --- a/FreeAPSWatch WatchKit Extension/WatchStateModel.swift +++ b/FreeAPSWatch WatchKit Extension/WatchStateModel.swift @@ -34,6 +34,7 @@ class WatchStateModel: NSObject, ObservableObject { @Published var isBolusViewActive = false @Published var displayOnWatch: AwConfig = .BGTarget @Published var displayFatAndProteinOnWatch = false + @Published var useNewCalc = false @Published var eventualBG = "" @Published var isConfirmationViewActive = false { didSet { @@ -174,6 +175,7 @@ class WatchStateModel: NSObject, ObservableObject { eventualBG = state.eventualBG ?? "" displayOnWatch = state.displayOnWatch ?? .BGTarget displayFatAndProteinOnWatch = state.displayFatAndProteinOnWatch ?? false + useNewCalc = state.useNewCalc ?? false isf = state.isf override = state.override }