Skip to content

Commit e91a2a5

Browse files
committed
Merge branch 'dev' into github-build-expiration-date
2 parents b6eb08b + 3367e60 commit e91a2a5

File tree

5 files changed

+118
-28
lines changed

5 files changed

+118
-28
lines changed

Loop/Managers/LoopDataManager.swift

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -910,8 +910,8 @@ extension LoopDataManager {
910910
notify(forChange: .loopFinished)
911911

912912
if FeatureFlags.missedMealNotifications {
913-
let carbEffectStart = now().addingTimeInterval(-MissedMealSettings.maxRecency)
914-
carbStore.getGlucoseEffects(start: carbEffectStart, end: now(), effectVelocities: insulinCounteractionEffects) {[weak self] result in
913+
let samplesStart = now().addingTimeInterval(-MissedMealSettings.maxRecency)
914+
carbStore.getGlucoseEffects(start: samplesStart, end: now(), effectVelocities: insulinCounteractionEffects) {[weak self] result in
915915
guard
916916
let self = self,
917917
case .success((_, let carbEffects)) = result
@@ -921,15 +921,28 @@ extension LoopDataManager {
921921
}
922922
return
923923
}
924-
925-
self.mealDetectionManager.generateMissedMealNotificationIfNeeded(
926-
insulinCounteractionEffects: self.insulinCounteractionEffects,
927-
carbEffects: carbEffects,
928-
pendingAutobolusUnits: self.recommendedAutomaticDose?.recommendation.bolusUnits,
929-
bolusDurationEstimator: { [unowned self] bolusAmount in
930-
return self.delegate?.loopDataManager(self, estimateBolusDuration: bolusAmount)
924+
925+
glucoseStore.getGlucoseSamples(start: samplesStart, end: now()) {[weak self] result in
926+
guard
927+
let self = self,
928+
case .success(let glucoseSamples) = result
929+
else {
930+
if case .failure(let error) = result {
931+
self?.logger.error("Failed to fetch glucose samples to check for missed meal: %{public}@", String(describing: error))
932+
}
933+
return
931934
}
932-
)
935+
936+
self.mealDetectionManager.generateMissedMealNotificationIfNeeded(
937+
glucoseSamples: glucoseSamples,
938+
insulinCounteractionEffects: self.insulinCounteractionEffects,
939+
carbEffects: carbEffects,
940+
pendingAutobolusUnits: self.recommendedAutomaticDose?.recommendation.bolusUnits,
941+
bolusDurationEstimator: { [unowned self] bolusAmount in
942+
return self.delegate?.loopDataManager(self, estimateBolusDuration: bolusAmount)
943+
}
944+
)
945+
}
933946
}
934947
}
935948

Loop/Managers/Missed Meal Detection/MealDetectionManager.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,22 @@ class MealDetectionManager {
6666
}
6767

6868
// MARK: Meal Detection
69-
func hasMissedMeal(insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], completion: @escaping (MissedMealStatus) -> Void) {
69+
func hasMissedMeal(glucoseSamples: [some GlucoseSampleValue], insulinCounteractionEffects: [GlucoseEffectVelocity], carbEffects: [GlucoseEffect], completion: @escaping (MissedMealStatus) -> Void) {
7070
let delta = TimeInterval(minutes: 5)
7171

7272
let intervalStart = currentDate(timeIntervalSinceNow: -MissedMealSettings.maxRecency)
7373
let intervalEnd = currentDate(timeIntervalSinceNow: -MissedMealSettings.minRecency)
7474
let now = self.currentDate
75-
75+
76+
let filteredGlucoseValues = glucoseSamples.filter { intervalStart <= $0.startDate && $0.startDate <= now }
77+
78+
/// Only try to detect if there's a missed meal if there are no calibration/user-entered BGs,
79+
/// since these can cause large jumps
80+
guard !filteredGlucoseValues.containsUserEntered() else {
81+
completion(.noMissedMeal)
82+
return
83+
}
84+
7685
let filteredCarbEffects = carbEffects.filterDateRange(intervalStart, now)
7786

7887
/// Compute how much of the ICE effect we can't explain via our entered carbs
@@ -214,12 +223,13 @@ class MealDetectionManager {
214223
/// - pendingAutobolusUnits: any autobolus units that are still being delivered. Used to delay the missed meal notification to avoid notifying during an autobolus.
215224
/// - bolusDurationEstimator: estimator of bolus duration that takes the units of the bolus as an input. Used to delay the missed meal notification to avoid notifying during an autobolus.
216225
func generateMissedMealNotificationIfNeeded(
226+
glucoseSamples: [some GlucoseSampleValue],
217227
insulinCounteractionEffects: [GlucoseEffectVelocity],
218228
carbEffects: [GlucoseEffect],
219229
pendingAutobolusUnits: Double? = nil,
220230
bolusDurationEstimator: @escaping (Double) -> TimeInterval?
221231
) {
222-
hasMissedMeal(insulinCounteractionEffects: insulinCounteractionEffects, carbEffects: carbEffects) {[weak self] status in
232+
hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: insulinCounteractionEffects, carbEffects: carbEffects) {[weak self] status in
223233
self?.manageMealNotifications(for: status, pendingAutobolusUnits: pendingAutobolusUnits, bolusDurationEstimator: bolusDurationEstimator)
224234
}
225235
}
@@ -295,3 +305,11 @@ class MealDetectionManager {
295305
completionHandler(report.joined(separator: "\n"))
296306
}
297307
}
308+
309+
fileprivate extension BidirectionalCollection where Element: GlucoseSampleValue, Index == Int {
310+
/// Returns whether there are any user-entered or calibration points
311+
/// Runtime: O(n)
312+
func containsUserEntered() -> Bool {
313+
return containsCalibrations() || filter({ $0.wasUserEntered }).count != 0
314+
}
315+
}

Loop/View Controllers/InsulinDeliveryTableViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ fileprivate var numberFormatter: NumberFormatter {
609609
fileprivate func createAttributedDescription(from description: String, with font: UIFont) -> NSAttributedString? {
610610
let descriptionWithFont = String(format:"<style>body{font-family: '-apple-system', '\(font.fontName)'; font-size: \(font.pointSize);}</style>%@", description)
611611

612-
guard let attributedDescription = try? NSMutableAttributedString(data: Data(descriptionWithFont.utf8), options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else {
612+
guard let attributedDescription = try? NSMutableAttributedString(data: descriptionWithFont.data(using: .utf16)!, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else {
613613
return nil
614614
}
615615

Loop/View Models/CarbEntryViewModel.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,13 +290,14 @@ final class CarbEntryViewModel: ObservableObject {
290290
}
291291

292292
private func checkIfOverrideEnabled() {
293-
if let managerSettings = delegate?.settings {
294-
if let overrideSettings = managerSettings.scheduleOverride?.settings, overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 {
295-
self.warnings.insert(.overrideInProgress)
296-
}
297-
else {
298-
self.warnings.remove(.overrideInProgress)
299-
}
293+
if let managerSettings = delegate?.settings,
294+
managerSettings.scheduleOverrideEnabled(at: Date()),
295+
let overrideSettings = managerSettings.scheduleOverride?.settings,
296+
overrideSettings.effectiveInsulinNeedsScaleFactor != 1.0 {
297+
self.warnings.insert(.overrideInProgress)
298+
}
299+
else {
300+
self.warnings.remove(.overrideInProgress)
300301
}
301302
}
302303

LoopTests/Managers/MealDetectionManagerTests.swift

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ import LoopCore
1212
import LoopKit
1313
@testable import Loop
1414

15+
fileprivate class MockGlucoseSample: GlucoseSampleValue {
16+
let provenanceIdentifier = ""
17+
let isDisplayOnly: Bool
18+
let wasUserEntered: Bool
19+
let condition: LoopKit.GlucoseCondition? = nil
20+
let trendRate: HKQuantity? = nil
21+
let quantity: HKQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 100)
22+
let startDate: Date
23+
24+
init(startDate: Date, isDisplayOnly: Bool = false, wasUserEntered: Bool = false) {
25+
self.startDate = startDate
26+
self.isDisplayOnly = isDisplayOnly
27+
self.wasUserEntered = wasUserEntered
28+
}
29+
}
30+
1531
enum MissedMealTestType {
1632
private static var dateFormatter = ISO8601DateFormatter.localTimeDate()
1733

@@ -175,6 +191,8 @@ class MealDetectionManagerTests: XCTestCase {
175191
var bolusUnits: Double?
176192
var bolusDurationEstimator: ((Double) -> TimeInterval?)!
177193

194+
fileprivate var glucoseSamples: [MockGlucoseSample]!
195+
178196
@discardableResult func setUp(for testType: MissedMealTestType) -> [GlucoseEffectVelocity] {
179197
carbStore = CarbStore(
180198
cacheStore: PersistenceController(directoryURL: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true)),
@@ -209,6 +227,8 @@ class MealDetectionManagerTests: XCTestCase {
209227
test_currentDate: testType.currentDate
210228
)
211229

230+
glucoseSamples = [MockGlucoseSample(startDate: now)]
231+
212232
bolusDurationEstimator = { units in
213233
self.bolusUnits = units
214234
return self.pumpManager.estimatedDuration(toBolus: units)
@@ -263,7 +283,7 @@ class MealDetectionManagerTests: XCTestCase {
263283

264284
let updateGroup = DispatchGroup()
265285
updateGroup.enter()
266-
mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
286+
mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
267287
XCTAssertEqual(status, .noMissedMeal)
268288
updateGroup.leave()
269289
}
@@ -275,7 +295,7 @@ class MealDetectionManagerTests: XCTestCase {
275295

276296
let updateGroup = DispatchGroup()
277297
updateGroup.enter()
278-
mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
298+
mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
279299
XCTAssertEqual(status, .noMissedMeal)
280300
updateGroup.leave()
281301
}
@@ -288,7 +308,7 @@ class MealDetectionManagerTests: XCTestCase {
288308

289309
let updateGroup = DispatchGroup()
290310
updateGroup.enter()
291-
mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
311+
mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
292312
XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 55))
293313
updateGroup.leave()
294314
}
@@ -301,7 +321,7 @@ class MealDetectionManagerTests: XCTestCase {
301321

302322
let updateGroup = DispatchGroup()
303323
updateGroup.enter()
304-
mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
324+
mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
305325
XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 25))
306326
updateGroup.leave()
307327
}
@@ -314,7 +334,7 @@ class MealDetectionManagerTests: XCTestCase {
314334

315335
let updateGroup = DispatchGroup()
316336
updateGroup.enter()
317-
mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
337+
mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
318338
XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 50))
319339
updateGroup.leave()
320340
}
@@ -326,7 +346,7 @@ class MealDetectionManagerTests: XCTestCase {
326346

327347
let updateGroup = DispatchGroup()
328348
updateGroup.enter()
329-
mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
349+
mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
330350
XCTAssertEqual(status, .noMissedMeal)
331351
updateGroup.leave()
332352
}
@@ -339,7 +359,7 @@ class MealDetectionManagerTests: XCTestCase {
339359

340360
let updateGroup = DispatchGroup()
341361
updateGroup.enter()
342-
mealDetectionManager.hasMissedMeal(insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
362+
mealDetectionManager.hasMissedMeal(glucoseSamples: glucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
343363
XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 40))
344364
updateGroup.leave()
345365
}
@@ -460,6 +480,44 @@ class MealDetectionManagerTests: XCTestCase {
460480
XCTAssertEqual(bolusUnits, 4.5)
461481
XCTAssertEqual(mealDetectionManager.lastMissedMealNotification?.deliveryTime, expectedDeliveryTime2)
462482
}
483+
484+
func testHasCalibrationPoints_NoNotification() {
485+
let testType = MissedMealTestType.manyMeals
486+
let counteractionEffects = setUp(for: testType)
487+
488+
let calibratedGlucoseSamples = [MockGlucoseSample(startDate: now), MockGlucoseSample(startDate: now, isDisplayOnly: true)]
489+
490+
let updateGroup = DispatchGroup()
491+
updateGroup.enter()
492+
mealDetectionManager.hasMissedMeal(glucoseSamples: calibratedGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
493+
XCTAssertEqual(status, .noMissedMeal)
494+
updateGroup.leave()
495+
}
496+
updateGroup.wait()
497+
498+
let manualGlucoseSamples = [MockGlucoseSample(startDate: now), MockGlucoseSample(startDate: now, wasUserEntered: true)]
499+
updateGroup.enter()
500+
mealDetectionManager.hasMissedMeal(glucoseSamples: manualGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
501+
XCTAssertEqual(status, .noMissedMeal)
502+
updateGroup.leave()
503+
}
504+
updateGroup.wait()
505+
}
506+
507+
func testHasTooOldCalibrationPoint_NoImpactOnNotificationDelivery() {
508+
let testType = MissedMealTestType.manyMeals
509+
let counteractionEffects = setUp(for: testType)
510+
511+
let tooOldCalibratedGlucoseSamples = [MockGlucoseSample(startDate: now, isDisplayOnly: false), MockGlucoseSample(startDate: now.addingTimeInterval(-MissedMealSettings.maxRecency-1), isDisplayOnly: true)]
512+
513+
let updateGroup = DispatchGroup()
514+
updateGroup.enter()
515+
mealDetectionManager.hasMissedMeal(glucoseSamples: tooOldCalibratedGlucoseSamples, insulinCounteractionEffects: counteractionEffects, carbEffects: mealDetectionCarbEffects(using: counteractionEffects)) { status in
516+
XCTAssertEqual(status, .hasMissedMeal(startTime: testType.missedMealDate!, carbAmount: 40))
517+
updateGroup.leave()
518+
}
519+
updateGroup.wait()
520+
}
463521
}
464522

465523
extension MealDetectionManagerTests {

0 commit comments

Comments
 (0)