Skip to content

Commit fc27085

Browse files
author
Darin Krauss
authored
LOOP-1146 Capture recommended bolus with actual user-entered bolus (#277)
- https://tidepool.atlassian.net/browse/LOOP-1146 - Add context date to SetBolusUserInfo - Store bolus dosing decision in LoopDataManager - Associate bolus dosing decision with current context in WatchDataManager - Add BolusDosingDecision - Update StoredDosingDecision Codable conformance - Capture bolus dosing decision in carb and bolus workflows - Additional tests
1 parent 0e557a9 commit fc27085

File tree

12 files changed

+460
-31
lines changed

12 files changed

+460
-31
lines changed

Common/Models/SetBolusUserInfo.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import LoopKit
1313
struct SetBolusUserInfo {
1414
let value: Double
1515
let startDate: Date
16+
let contextDate: Date?
1617
let carbEntry: NewCarbEntry?
1718
}
1819

@@ -34,6 +35,7 @@ extension SetBolusUserInfo: RawRepresentable {
3435

3536
self.value = value
3637
self.startDate = startDate
38+
self.contextDate = rawValue["cd"] as? Date
3739
self.carbEntry = (rawValue["ce"] as? NewCarbEntry.RawValue).flatMap(NewCarbEntry.init(rawValue:))
3840
}
3941

@@ -45,9 +47,8 @@ extension SetBolusUserInfo: RawRepresentable {
4547
"sd": startDate
4648
]
4749

48-
if let carbEntry = carbEntry {
49-
raw["ce"] = carbEntry.rawValue
50-
}
50+
raw["cd"] = contextDate
51+
raw["ce"] = carbEntry?.rawValue
5152

5253
return raw
5354
}

Loop.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@
377377
A9347F3124E7521800C99C34 /* CarbBackfillRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9347F3024E7521800C99C34 /* CarbBackfillRequestUserInfo.swift */; };
378378
A9347F3224E7522400C99C34 /* CarbBackfillRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9347F3024E7521800C99C34 /* CarbBackfillRequestUserInfo.swift */; };
379379
A9347F3324E7522900C99C34 /* WatchHistoricalCarbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9347F2E24E7508A00C99C34 /* WatchHistoricalCarbs.swift */; };
380+
A963B27A252CEBAE0062AA12 /* SetBolusUserInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A963B279252CEBAE0062AA12 /* SetBolusUserInfoTests.swift */; };
380381
A966152623EA5A26005D8B29 /* DefaultAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A966152423EA5A25005D8B29 /* DefaultAssets.xcassets */; };
381382
A966152723EA5A26005D8B29 /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A966152523EA5A25005D8B29 /* DerivedAssets.xcassets */; };
382383
A966152A23EA5A37005D8B29 /* DefaultAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A966152823EA5A37005D8B29 /* DefaultAssets.xcassets */; };
@@ -418,6 +419,7 @@
418419
A9F703732489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */; };
419420
A9F703752489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703742489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift */; };
420421
A9F703772489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703762489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift */; };
422+
A9FB75F1252BE320004C7D3F /* BolusDosingDecision.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */; };
421423
B405E35924D2A75B00DD058D /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A966152923EA5A37005D8B29 /* DerivedAssets.xcassets */; };
422424
B405E35A24D2B1A400DD058D /* HUDAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C15961E09E94E00E160D4 /* HUDAssets.xcassets */; };
423425
B405E35B24D2E05600DD058D /* HUDAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C15961E09E94E00E160D4 /* HUDAssets.xcassets */; };
@@ -1239,6 +1241,7 @@
12391241
A9347F2E24E7508A00C99C34 /* WatchHistoricalCarbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHistoricalCarbs.swift; sourceTree = "<group>"; };
12401242
A9347F3024E7521800C99C34 /* CarbBackfillRequestUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbBackfillRequestUserInfo.swift; sourceTree = "<group>"; };
12411243
A951C5FF23E8AB51003E26DC /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
1244+
A963B279252CEBAE0062AA12 /* SetBolusUserInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetBolusUserInfoTests.swift; sourceTree = "<group>"; };
12421245
A966152423EA5A25005D8B29 /* DefaultAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DefaultAssets.xcassets; sourceTree = "<group>"; };
12431246
A966152523EA5A25005D8B29 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = "<group>"; };
12441247
A966152823EA5A37005D8B29 /* DefaultAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DefaultAssets.xcassets; sourceTree = "<group>"; };
@@ -1278,6 +1281,7 @@
12781281
A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbStore+SimulatedCoreData.swift"; sourceTree = "<group>"; };
12791282
A9F703742489C9A000C98AD8 /* GlucoseStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GlucoseStore+SimulatedCoreData.swift"; sourceTree = "<group>"; };
12801283
A9F703762489D8AA00C98AD8 /* PersistentDeviceLog+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistentDeviceLog+SimulatedCoreData.swift"; sourceTree = "<group>"; };
1284+
A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusDosingDecision.swift; sourceTree = "<group>"; };
12811285
B40D07C6251A89D500C1C6D7 /* GlucoseDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDisplay.swift; sourceTree = "<group>"; };
12821286
B42C951324A3C76000857C73 /* CGMStatusHUDViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMStatusHUDViewModel.swift; sourceTree = "<group>"; };
12831287
B42C951624A3CAF200857C73 /* NotificationsCriticalAlertPermissionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsCriticalAlertPermissionsViewModel.swift; sourceTree = "<group>"; };
@@ -1605,6 +1609,7 @@
16051609
isa = PBXGroup;
16061610
children = (
16071611
43511CDD21FD80AD00566C63 /* RetrospectiveCorrection */,
1612+
A9FB75F0252BE320004C7D3F /* BolusDosingDecision.swift */,
16081613
C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */,
16091614
B40D07C6251A89D500C1C6D7 /* GlucoseDisplay.swift */,
16101615
43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */,
@@ -2323,6 +2328,7 @@
23232328
children = (
23242329
A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */,
23252330
A9E6DFEE246A0474005B1A1C /* LoopErrorTests.swift */,
2331+
A963B279252CEBAE0062AA12 /* SetBolusUserInfoTests.swift */,
23262332
A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */,
23272333
);
23282334
path = Models;
@@ -3377,6 +3383,7 @@
33773383
4F70C2101DE8FAC5006380B7 /* StatusExtensionDataManager.swift in Sources */,
33783384
43DFB62320D4CAE7008A7BAE /* PumpManager.swift in Sources */,
33793385
B45E704D25275023000C0E68 /* AdverseEventReportViewModel.swift in Sources */,
3386+
A9FB75F1252BE320004C7D3F /* BolusDosingDecision.swift in Sources */,
33803387
892A5D59222F0A27008961AB /* Debug.swift in Sources */,
33813388
1DD0B76724EC77AC008A2DC3 /* SupportScreenView.swift in Sources */,
33823389
431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */,
@@ -3580,6 +3587,7 @@
35803587
1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */,
35813588
A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */,
35823589
A9DFAFB324F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift in Sources */,
3590+
A963B27A252CEBAE0062AA12 /* SetBolusUserInfoTests.swift in Sources */,
35833591
A985464D251448300099C1A6 /* OutputStreamTests.swift in Sources */,
35843592
A9A63F8D246B261100588D5B /* DosingDecisionStoreTests.swift in Sources */,
35853593
A9E6DFEF246A0474005B1A1C /* LoopErrorTests.swift in Sources */,

Loop/Extensions/DeviceDataManager+BolusEntryViewModelDelegate.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ extension DeviceDataManager: BolusEntryViewModelDelegate {
2020
loopManager.addGlucose(samples, completion: completion)
2121
}
2222

23-
func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry?, completion: @escaping (Result<Void>) -> Void) {
23+
func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry?, completion: @escaping (Result<StoredCarbEntry>) -> Void) {
2424
loopManager.addCarbEntry(carbEntry, replacing: replacingEntry, completion: completion)
2525
}
2626

27+
func storeBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) {
28+
loopManager.storeBolusDosingDecision(bolusDosingDecision, withDate: date)
29+
}
30+
2731
/// func enactBolus(units: Double, at startDate: Date, completion: @escaping (_ error: Error?) -> Void)
2832
/// is already implemented in DeviceDataManager
2933

Loop/Managers/LoopDataManager.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,17 +487,17 @@ extension LoopDataManager {
487487
/// - carbEntry: The new carb value
488488
/// - completion: A closure called once upon completion
489489
/// - result: The bolus recommendation
490-
func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry? = nil, completion: @escaping (_ result: Result<Void>) -> Void) {
490+
func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry? = nil, completion: @escaping (_ result: Result<StoredCarbEntry>) -> Void) {
491491
let addCompletion: (CarbStoreResult<StoredCarbEntry>) -> Void = { (result) in
492492
self.dataAccessQueue.async {
493493
switch result {
494-
case .success:
494+
case .success(let storedCarbEntry):
495495
// Remove the active pre-meal target override
496496
self.settings.clearOverride(matching: .preMeal)
497497

498498
self.carbEffect = nil
499499
self.carbsOnBoard = nil
500-
completion(.success(()))
500+
completion(.success(storedCarbEntry))
501501
case .failure(let error):
502502
completion(.failure(error))
503503
}
@@ -647,6 +647,22 @@ extension LoopDataManager {
647647
}
648648
}
649649

650+
func storeBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) {
651+
let dosingDecision = StoredDosingDecision(date: date,
652+
insulinOnBoard: bolusDosingDecision.insulinOnBoard,
653+
carbsOnBoard: bolusDosingDecision.carbsOnBoard,
654+
scheduleOverride: bolusDosingDecision.scheduleOverride,
655+
glucoseTargetRangeSchedule: bolusDosingDecision.glucoseTargetRangeSchedule,
656+
glucoseTargetRangeScheduleApplyingOverrideIfActive: bolusDosingDecision.glucoseTargetRangeScheduleApplyingOverrideIfActive,
657+
predictedGlucoseIncludingPendingInsulin: bolusDosingDecision.predictedGlucoseIncludingPendingInsulin,
658+
manualGlucose: bolusDosingDecision.manualGlucose.map { SimpleGlucoseValue($0) },
659+
originalCarbEntry: bolusDosingDecision.originalCarbEntry,
660+
carbEntry: bolusDosingDecision.carbEntry,
661+
recommendedBolus: bolusDosingDecision.recommendedBolus.map { StoredDosingDecision.BolusRecommendationWithDate(recommendation: $0, date: date) },
662+
requestedBolus: bolusDosingDecision.requestedBolus)
663+
self.dosingDecisionStore.storeDosingDecision(dosingDecision) {}
664+
}
665+
650666
func storeSettings() {
651667
guard let appGroup = UserDefaults.appGroup, let loopSettings = appGroup.loopSettings else {
652668
return

Loop/Managers/WatchDataManager.swift

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ final class WatchDataManager: NSObject {
4444
private var lastSentSettings: LoopSettings?
4545
private var lastSentBolusVolumes: [Double]?
4646

47+
private var contextDosingDecisions: [Date: BolusDosingDecision] {
48+
get { lockedContextDosingDecisions.value }
49+
set { lockedContextDosingDecisions.value = newValue }
50+
}
51+
private var lockedContextDosingDecisions: Locked<[Date: BolusDosingDecision]> = Locked([:])
52+
53+
private let contextDosingDecisionExpirationDuration: TimeInterval = -.minutes(5)
54+
4755
let sleepStore: SleepStore
4856

4957
var lastBedtimeQuery: Date {
@@ -215,6 +223,8 @@ final class WatchDataManager: NSObject {
215223
}
216224

217225
private func createWatchContext(recommendingBolusFor potentialCarbEntry: NewCarbEntry? = nil, _ completion: @escaping (_ context: WatchContext) -> Void) {
226+
var dosingDecision = BolusDosingDecision()
227+
218228
let loopManager = deviceManager.loopManager!
219229

220230
let glucose = deviceManager.glucoseStore.latestGlucose
@@ -223,13 +233,20 @@ final class WatchDataManager: NSObject {
223233

224234
loopManager.getLoopState { (manager, state) in
225235
let updateGroup = DispatchGroup()
236+
237+
let carbsOnBoard = state.carbsOnBoard
238+
let recommendedBolus = state.recommendedBolus
239+
226240
let context = WatchContext(glucose: glucose, glucoseUnit: self.deviceManager.glucoseStore.preferredUnit)
227241
context.reservoir = reservoir?.unitVolume
228242
context.loopLastRunDate = manager.lastLoopCompleted
229-
context.recommendedBolusDose = state.recommendedBolus?.recommendation.amount
230-
context.cob = state.carbsOnBoard?.quantity.doubleValue(for: HKUnit.gram())
243+
context.recommendedBolusDose = recommendedBolus?.recommendation.amount
244+
context.cob = carbsOnBoard?.quantity.doubleValue(for: HKUnit.gram())
231245
context.glucoseTrendRawValue = self.deviceManager.glucoseDisplay(for: glucose)?.trendType?.rawValue
232246

247+
dosingDecision.carbsOnBoard = carbsOnBoard
248+
dosingDecision.recommendedBolus = recommendedBolus?.recommendation
249+
233250
context.cgmManagerState = self.deviceManager.cgmManager?.rawValue
234251

235252
if let trend = self.deviceManager.cgmManager?.glucoseDisplay?.trendType {
@@ -238,7 +255,11 @@ final class WatchDataManager: NSObject {
238255

239256
if let potentialCarbEntry = potentialCarbEntry {
240257
context.potentialCarbEntry = potentialCarbEntry
241-
context.recommendedBolusDoseConsideringPotentialCarbEntry = try? state.recommendBolus(consideringPotentialCarbEntry: potentialCarbEntry, replacingCarbEntry: nil)?.amount
258+
if let recommendedBolusDoseConsideringPotentialCarbEntry = try? state.recommendBolus(consideringPotentialCarbEntry: potentialCarbEntry, replacingCarbEntry: nil) {
259+
context.recommendedBolusDoseConsideringPotentialCarbEntry = recommendedBolusDoseConsideringPotentialCarbEntry.amount
260+
dosingDecision.recommendedBolus = recommendedBolusDoseConsideringPotentialCarbEntry
261+
}
262+
242263
}
243264

244265
if let glucose = glucose {
@@ -260,8 +281,10 @@ final class WatchDataManager: NSObject {
260281
switch result {
261282
case .success(let iobValue):
262283
context.iob = iobValue.value
284+
dosingDecision.insulinOnBoard = iobValue
263285
case .failure:
264286
context.iob = nil
287+
dosingDecision.insulinOnBoard = nil
265288
}
266289
updateGroup.leave()
267290
}
@@ -273,12 +296,42 @@ final class WatchDataManager: NSObject {
273296
context.lastNetTempBasalDose = netBasal.rate
274297
}
275298

276-
// Drop the first element in predictedGlucose because it is the current glucose
277-
if let predictedGlucose = state.predictedGlucoseIncludingPendingInsulin?.dropFirst(), predictedGlucose.count > 0 {
278-
context.predictedGlucose = WatchPredictedGlucose(values: Array(predictedGlucose))
299+
if let predictedGlucose = state.predictedGlucoseIncludingPendingInsulin {
300+
dosingDecision.predictedGlucoseIncludingPendingInsulin = predictedGlucose
301+
302+
// Drop the first element in predictedGlucose because it is the current glucose
303+
let filteredPredictedGlucose = predictedGlucose.dropFirst()
304+
if filteredPredictedGlucose.count > 0 {
305+
context.predictedGlucose = WatchPredictedGlucose(values: Array(filteredPredictedGlucose))
306+
}
307+
}
308+
309+
let settings = self.deviceManager.loopManager.settings
310+
311+
var preMealOverride = settings.preMealOverride
312+
if preMealOverride?.hasFinished() == true {
313+
preMealOverride = nil
314+
}
315+
316+
var scheduleOverride = settings.scheduleOverride
317+
if scheduleOverride?.hasFinished() == true {
318+
scheduleOverride = nil
319+
}
320+
321+
dosingDecision.scheduleOverride = preMealOverride ?? scheduleOverride
322+
dosingDecision.glucoseTargetRangeSchedule = settings.glucoseTargetRangeSchedule
323+
if scheduleOverride != nil || preMealOverride != nil {
324+
dosingDecision.glucoseTargetRangeScheduleApplyingOverrideIfActive = settings.glucoseTargetRangeScheduleApplyingOverrideIfActive
325+
} else {
326+
dosingDecision.glucoseTargetRangeScheduleApplyingOverrideIfActive = nil
279327
}
280328

281329
_ = updateGroup.wait(timeout: .distantFuture)
330+
331+
// Remove any expired context dosing decisions and add new
332+
self.contextDosingDecisions = self.contextDosingDecisions.filter { (date, _) in date.timeIntervalSinceNow > self.contextDosingDecisionExpirationDuration }
333+
self.contextDosingDecisions[context.creationDate] = dosingDecision
334+
282335
completion(context)
283336
}
284337
}
@@ -289,7 +342,17 @@ final class WatchDataManager: NSObject {
289342
return
290343
}
291344

345+
var dosingDecision: BolusDosingDecision
346+
if let contextDate = bolus.contextDate, let contextDosingDecision = contextDosingDecisions[contextDate] {
347+
dosingDecision = contextDosingDecision
348+
} else {
349+
dosingDecision = BolusDosingDecision() // The user saved without waiting for recommendation (no bolus)
350+
}
351+
292352
func enactBolus() {
353+
dosingDecision.requestedBolus = bolus.value
354+
deviceManager.loopManager.storeBolusDosingDecision(dosingDecision, withDate: bolus.startDate)
355+
293356
guard bolus.value > 0 else {
294357
// Ensure active carbs is updated in the absence of a bolus
295358
sendWatchContextIfNeeded()
@@ -309,14 +372,16 @@ final class WatchDataManager: NSObject {
309372
if let carbEntry = bolus.carbEntry {
310373
deviceManager.loopManager.addCarbEntry(carbEntry) { (result) in
311374
switch result {
312-
case .success:
375+
case .success(let storedCarbEntry):
376+
dosingDecision.carbEntry = storedCarbEntry
313377
self.deviceManager.analyticsServicesManager.didAddCarbsFromWatch()
314378
enactBolus()
315379
case .failure(let error):
316380
self.log.error("%{public}@", String(describing: error))
317381
}
318382
}
319383
} else {
384+
dosingDecision.carbEntry = nil
320385
enactBolus()
321386
}
322387
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// BolusDosingDecision.swift
3+
// Loop
4+
//
5+
// Created by Darin Krauss on 10/1/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import LoopKit
10+
11+
struct BolusDosingDecision {
12+
var insulinOnBoard: InsulinValue?
13+
var carbsOnBoard: CarbValue?
14+
var scheduleOverride: TemporaryScheduleOverride?
15+
var glucoseTargetRangeSchedule: GlucoseRangeSchedule?
16+
var glucoseTargetRangeScheduleApplyingOverrideIfActive: GlucoseRangeSchedule?
17+
var predictedGlucoseIncludingPendingInsulin: [PredictedGlucoseValue]?
18+
var manualGlucose: GlucoseValue?
19+
var originalCarbEntry: StoredCarbEntry?
20+
var carbEntry: StoredCarbEntry?
21+
var recommendedBolus: BolusRecommendation?
22+
var requestedBolus: Double?
23+
24+
init() {}
25+
}

0 commit comments

Comments
 (0)