Skip to content

Add missed meal notifications to Loop #1825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 59 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
9f7a199
Add unannounced meal func to CarbStoreProtocol
novalegra Sep 19, 2022
1755573
V1 of UAM algo
novalegra Sep 20, 2022
f227352
Add debug logs
novalegra Sep 20, 2022
b42a834
Handle unannounced meal notification & open up controller
novalegra Sep 20, 2022
64247d2
Add mealstart to notification userinfo
novalegra Sep 20, 2022
4fdf7bb
Improve notification description
novalegra Sep 21, 2022
ed72d3c
Add direct entry to carb flow from notification on watch
novalegra Sep 21, 2022
d3e2c5d
Add auto-setting of carb entry on watch
novalegra Sep 21, 2022
49c6928
carbThreshold -> mealCarbThreshold
novalegra Oct 2, 2022
54118f9
Retract UAM notifications after the carbs have expired
novalegra Oct 2, 2022
e5e1145
Improve function naming
novalegra Oct 4, 2022
2416b24
Move notification logic from LoopKit to Loop
novalegra Oct 12, 2022
f34f4f9
Make current date configurable
novalegra Oct 17, 2022
1364e56
Make current date configurable during unit testing
novalegra Oct 18, 2022
52ca5f4
Update status enum naming for point-of-use clarity
novalegra Oct 18, 2022
2052be0
Merge remote-tracking branch 'origin/dev' into aq/missed-meal-detection
novalegra Oct 18, 2022
194fcac
Make 'now' time configurable
novalegra Oct 19, 2022
480c891
Extract loop data manager testing logic into base class
novalegra Oct 19, 2022
9b7b2d9
Extract loop data manager dosing tests into their own file
novalegra Oct 19, 2022
be4abd8
Add unannounced meal tests
novalegra Oct 19, 2022
c96c005
Fix concurrency issue
novalegra Oct 19, 2022
20202fb
Pull UAM constants into separate struct
novalegra Oct 19, 2022
a19311f
Fix notifications not being retracted after their expiration date
novalegra Oct 23, 2022
85cb4c5
Expire using start of meal instead of current time
novalegra Oct 23, 2022
c9195c6
Add unannounced meal notifications permission
novalegra Nov 8, 2022
2d9bf9f
Add AlertPermissionsViewModel
novalegra Nov 8, 2022
6449982
Remove unneeded `AnyView`s
novalegra Nov 8, 2022
5224f57
Removed unused completion block
novalegra Nov 8, 2022
759052a
Use the counteraction effects passed into the function
novalegra Nov 9, 2022
f43c7ec
Schedule missed meal notifications to avoid notification during an mi…
novalegra Nov 17, 2022
d0af6ad
Update `estimatedDuration` function headers in response to PR feedback
novalegra Nov 17, 2022
8a90307
Merge branch 'dev' into aq/missed-meal-detection
novalegra Nov 21, 2022
7a4cb45
Add UAM banner to carb entry screen (#5)
novalegra Nov 26, 2022
936110b
UAM algo updates: only use directly observed carb absorption (#6)
novalegra Nov 28, 2022
722c61c
Create `MealDetectionManager` from old UAM functions in `CarbStore`
novalegra Nov 29, 2022
a811c03
Move notification logic into `MealDetectionManager`
novalegra Nov 29, 2022
72a5ab3
Updates based on feedback for UAM PR (#7)
novalegra Nov 30, 2022
686bf70
Move UAM test fixtures from LoopKit to Loop
novalegra Nov 30, 2022
c4644cd
Merge branch 'uam-pr-feedback' into aq/missed-meal-detection
novalegra Nov 30, 2022
b719b12
Remove old TODO
novalegra Nov 30, 2022
a5dc1f2
Revert change to Loop signing team
novalegra Jan 8, 2023
41f38e3
Merge branch 'dev' into aq/missed-meal-detection
novalegra Jan 8, 2023
e91ae0d
Fix merge issues
novalegra Jan 8, 2023
352a148
Make tests runnable
novalegra Jan 10, 2023
fa2bbe2
Lower meal carb threshold to 15 g to reduce false-negatives
novalegra Jan 11, 2023
4f18d36
Merge remote-tracking branch 'origin/dev' into aq/missed-meal-detection
novalegra Jan 17, 2023
e3a5822
Fix carb entry controller merge issues
novalegra Jan 19, 2023
663997c
Merge branch 'dev' into aq/missed-meal-detection
novalegra Feb 10, 2023
9dfaf22
Unannounced meal / UAM -> missed meal
novalegra Feb 16, 2023
dbc0b96
UAM test fixture -> missed meal test fixture
novalegra Feb 17, 2023
5bff761
Merge remote-tracking branch 'origin/dev' into aq/missed-meal-detection
novalegra Feb 17, 2023
7b23e08
Variable naming improvements
novalegra Feb 18, 2023
d35c210
Remove `AlertPermissionsViewModel`
novalegra Feb 18, 2023
44a4557
Move call to check for missed meals to `loop()`-level
novalegra Feb 18, 2023
879a42c
Remove `CarbStore` dependency from `MealDetectionManager`
novalegra Feb 18, 2023
1692f55
Reduce `minRecency` required to detect a meal
novalegra Feb 18, 2023
e9e2384
Update counteraction effect math in `MealDetectionManager` to skew to…
novalegra Feb 19, 2023
4ddf464
Merge remote-tracking branch 'origin/dev' into aq/missed-meal-detection
novalegra Feb 20, 2023
cd3093f
Fix conflict + move missed meal toggle 1 level higher
novalegra Feb 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion Loop.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,10 @@ extension DeviceDataManager: LoopDataManagerDelegate {

return rounded
}

func loopDataManager(_ manager: LoopDataManager, estimateBolusDuration units: Double) -> TimeInterval? {
pumpManager?.estimatedDuration(toBolus: units)
}

func loopDataManager(
_ manager: LoopDataManager,
Expand Down
25 changes: 24 additions & 1 deletion Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ extension LoopAppManager: UNUserNotificationCenterDelegate {
LoopNotificationCategory.remoteBolus.rawValue,
LoopNotificationCategory.remoteBolusFailure.rawValue,
LoopNotificationCategory.remoteCarbs.rawValue,
LoopNotificationCategory.remoteCarbsFailure.rawValue:
LoopNotificationCategory.remoteCarbsFailure.rawValue,
LoopNotificationCategory.missedMeal.rawValue:
completionHandler([.badge, .sound, .list, .banner])
default:
// For all others, banners are not to be displayed while in the foreground
Expand Down Expand Up @@ -516,6 +517,28 @@ extension LoopAppManager: UNUserNotificationCenterDelegate {
let managerIdentifier = userInfo[LoopNotificationUserInfoKey.managerIDForAlert.rawValue] as? String {
alertManager?.acknowledgeAlert(identifier: Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: alertIdentifier))
}
case UNNotificationDefaultActionIdentifier:
guard response.notification.request.identifier == LoopNotificationCategory.missedMeal.rawValue else {
break
}

let carbActivity = NSUserActivity.forNewCarbEntry()
let userInfo = response.notification.request.content.userInfo

if
let mealTime = userInfo[LoopNotificationUserInfoKey.missedMealTime.rawValue] as? Date,
let carbAmount = userInfo[LoopNotificationUserInfoKey.missedMealCarbAmount.rawValue] as? Double
{
let missedEntry = NewCarbEntry(quantity: HKQuantity(unit: .gram(),
doubleValue: carbAmount),
startDate: mealTime,
foodType: nil,
absorptionTime: nil)
carbActivity.update(from: missedEntry, isMissedMeal: true)
}

rootViewController?.restoreUserActivityState(carbActivity)

default:
break
}
Expand Down
64 changes: 57 additions & 7 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ final class LoopDataManager {
static let LoopUpdateContextKey = "com.loudnate.Loop.LoopDataManager.LoopUpdateContext"

private let carbStore: CarbStoreProtocol

private let mealDetectionManager: MealDetectionManager

private let doseStore: DoseStoreProtocol

Expand Down Expand Up @@ -109,6 +111,11 @@ final class LoopDataManager {
self.now = now

self.latestStoredSettingsProvider = latestStoredSettingsProvider
self.mealDetectionManager = MealDetectionManager(
carbRatioScheduleApplyingOverrideHistory: carbStore.carbRatioScheduleApplyingOverrideHistory,
insulinSensitivityScheduleApplyingOverrideHistory: carbStore.insulinSensitivityScheduleApplyingOverrideHistory,
maximumBolus: settings.maximumBolus
)

self.lockedPumpInsulinType = Locked(pumpInsulinType)

Expand Down Expand Up @@ -259,11 +266,16 @@ final class LoopDataManager {

// Invalidate cached effects affected by the override
invalidateCachedEffects = true

// Update the affected schedules
mealDetectionManager.carbRatioScheduleApplyingOverrideHistory = carbRatioScheduleApplyingOverrideHistory
mealDetectionManager.insulinSensitivityScheduleApplyingOverrideHistory = insulinSensitivityScheduleApplyingOverrideHistory
}

if newValue.insulinSensitivitySchedule != oldValue.insulinSensitivitySchedule {
carbStore.insulinSensitivitySchedule = newValue.insulinSensitivitySchedule
doseStore.insulinSensitivitySchedule = newValue.insulinSensitivitySchedule
mealDetectionManager.insulinSensitivityScheduleApplyingOverrideHistory = insulinSensitivityScheduleApplyingOverrideHistory
invalidateCachedEffects = true
analyticsServicesManager.didChangeInsulinSensitivitySchedule()
}
Expand All @@ -278,6 +290,7 @@ final class LoopDataManager {

if newValue.carbRatioSchedule != oldValue.carbRatioSchedule {
carbStore.carbRatioSchedule = newValue.carbRatioSchedule
mealDetectionManager.carbRatioScheduleApplyingOverrideHistory = carbRatioScheduleApplyingOverrideHistory
invalidateCachedEffects = true
analyticsServicesManager.didChangeCarbRatioSchedule()
}
Expand All @@ -292,6 +305,10 @@ final class LoopDataManager {
analyticsServicesManager.didChangeInsulinModel()
}

if newValue.maximumBolus != oldValue.maximumBolus {
mealDetectionManager.maximumBolus = newValue.maximumBolus
}

if invalidateCachedEffects {
dataAccessQueue.async {
// Invalidate cached effects based on this schedule
Expand Down Expand Up @@ -857,6 +874,28 @@ extension LoopDataManager {
logger.default("Loop ended")
notify(forChange: .loopFinished)

let carbEffectStart = now().addingTimeInterval(-MissedMealSettings.maxRecency)
carbStore.getGlucoseEffects(start: carbEffectStart, end: now(), effectVelocities: insulinCounteractionEffects) {[weak self] result in
guard
let self = self,
case .success((_, let carbEffects)) = result
else {
if case .failure(let error) = result {
self?.logger.error("Failed to fetch glucose effects to check for missed meal: %{public}@", String(describing: error))
}
return
}

self.mealDetectionManager.generateMissedMealNotificationIfNeeded(
insulinCounteractionEffects: self.insulinCounteractionEffects,
carbEffects: carbEffects,
pendingAutobolusUnits: self.recommendedAutomaticDose?.recommendation.bolusUnits,
bolusDurationEstimator: { [unowned self] bolusAmount in
return self.delegate?.loopDataManager(self, estimateBolusDuration: bolusAmount)
}
)
}

// 5 second delay to allow stores to cache data before it is read by widget
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.widgetLog.default("Refreshing widget. Reason: Loop completed")
Expand Down Expand Up @@ -1764,7 +1803,6 @@ extension LoopDataManager {
}
}
}

}

/// Describes a view into the loop state
Expand Down Expand Up @@ -2104,16 +2142,21 @@ extension LoopDataManager {
self.doseStore.generateDiagnosticReport { (report) in
entries.append(report)
entries.append("")

UNUserNotificationCenter.current().generateDiagnosticReport { (report) in
self.mealDetectionManager.generateDiagnosticReport { report in
entries.append(report)
entries.append("")

UIDevice.current.generateDiagnosticReport { (report) in
UNUserNotificationCenter.current().generateDiagnosticReport { (report) in
entries.append(report)
entries.append("")

completion(entries.joined(separator: "\n"))
UIDevice.current.generateDiagnosticReport { (report) in
entries.append(report)
entries.append("")

completion(entries.joined(separator: "\n"))
}
}
}
}
Expand Down Expand Up @@ -2147,7 +2190,14 @@ protocol LoopDataManagerDelegate: AnyObject {
/// - rate: The recommended rate in U/hr
/// - Returns: a supported rate of delivery in Units/hr. The rate returned should not be larger than the passed in rate.
func roundBasalRate(unitsPerHour: Double) -> Double


/// Asks the delegate to estimate the duration to deliver the bolus.
///
/// - Parameters:
/// - bolusUnits: size of the bolus in U
/// - Returns: the estimated time it will take to deliver bolus
func loopDataManager(_ manager: LoopDataManager, estimateBolusDuration bolusUnits: Double) -> TimeInterval?

/// Asks the delegate to round a recommended bolus volume to a supported volume
///
/// - Parameters:
Expand Down
Loading