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
}