|
@@ -40,6 +40,7 @@ final class GlucoseAlertCoordinator: Injectable {
|
|
|
|
|
|
|
|
/// Pure breach predicate. Low family (low/urgentLow/forecastedLow) breaches
|
|
/// Pure breach predicate. Low family (low/urgentLow/forecastedLow) breaches
|
|
|
/// when the value is at or below threshold; high breaches at or above.
|
|
/// when the value is at or below threshold; high breaches at or above.
|
|
|
|
|
+ /// `carbsRequired` is determination-driven and evaluated separately.
|
|
|
/// Extracted for unit testing — the instance evaluators call through here.
|
|
/// Extracted for unit testing — the instance evaluators call through here.
|
|
|
static func breached(type: GlucoseAlertType, latestMgDL: Decimal, thresholdMgDL: Decimal) -> Bool {
|
|
static func breached(type: GlucoseAlertType, latestMgDL: Decimal, thresholdMgDL: Decimal) -> Bool {
|
|
|
switch type {
|
|
switch type {
|
|
@@ -49,6 +50,8 @@ final class GlucoseAlertCoordinator: Injectable {
|
|
|
return latestMgDL <= thresholdMgDL
|
|
return latestMgDL <= thresholdMgDL
|
|
|
case .high:
|
|
case .high:
|
|
|
return latestMgDL >= thresholdMgDL
|
|
return latestMgDL >= thresholdMgDL
|
|
|
|
|
+ case .carbsRequired:
|
|
|
|
|
+ return false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -68,6 +71,8 @@ final class GlucoseAlertCoordinator: Injectable {
|
|
|
return latestMgDL >= thresholdMgDL + recoveryMarginMgDL
|
|
return latestMgDL >= thresholdMgDL + recoveryMarginMgDL
|
|
|
case .high:
|
|
case .high:
|
|
|
return latestMgDL <= thresholdMgDL - recoveryMarginMgDL
|
|
return latestMgDL <= thresholdMgDL - recoveryMarginMgDL
|
|
|
|
|
+ case .carbsRequired:
|
|
|
|
|
+ return false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -153,7 +158,7 @@ final class GlucoseAlertCoordinator: Injectable {
|
|
|
// would surface two in-app alerts for the same low event.
|
|
// would surface two in-app alerts for the same low event.
|
|
|
let sorted = snapshot.sorted { $0.type.priority < $1.type.priority }
|
|
let sorted = snapshot.sorted { $0.type.priority < $1.type.priority }
|
|
|
var urgentLowFiring = false
|
|
var urgentLowFiring = false
|
|
|
- for alarm in sorted where alarm.type != .forecastedLow {
|
|
|
|
|
|
|
+ for alarm in sorted where alarm.type.isReadingDriven {
|
|
|
if alarm.type == .low, urgentLowFiring {
|
|
if alarm.type == .low, urgentLowFiring {
|
|
|
retractIfFiring(alarm)
|
|
retractIfFiring(alarm)
|
|
|
continue
|
|
continue
|
|
@@ -244,6 +249,35 @@ final class GlucoseAlertCoordinator: Injectable {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // MARK: - Carbs-required evaluation
|
|
|
|
|
+
|
|
|
|
|
+ /// Mirrors `evaluateForecast` for the carbs-required alarms. The alarm
|
|
|
|
|
+ /// threshold (grams, stored in `thresholdMgDL`) is independent from the
|
|
|
|
|
+ /// tab badge's threshold — surfaces are configured separately so the
|
|
|
|
|
+ /// user can have a low-threshold badge for awareness and a higher-
|
|
|
|
|
+ /// threshold alarm for active interruption.
|
|
|
|
|
+ private func evaluateCarbsRequired(_ determination: Determination) {
|
|
|
|
|
+ guard !isInLaunchQuietWindow else { return }
|
|
|
|
|
+ let snapshot = alertsSnapshot
|
|
|
|
|
+ let configuration = configurationSnapshot
|
|
|
|
|
+ let now = Date()
|
|
|
|
|
+ let carbsReq: Decimal? = determination.carbsReq
|
|
|
|
|
+
|
|
|
|
|
+ for alarm in snapshot where alarm.type == .carbsRequired {
|
|
|
|
|
+ guard alarm.shouldEvaluate, !isAlarmSnoozed(alarm, at: now),
|
|
|
|
|
+ isActive(alarm, at: now, configuration: configuration)
|
|
|
|
|
+ else {
|
|
|
|
|
+ retractIfFiring(alarm)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ guard let carbs = carbsReq, carbs >= alarm.thresholdMgDL else {
|
|
|
|
|
+ retractIfFiring(alarm)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ fireIfNeeded(alarm, valueMgDL: carbs)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// MARK: - Issue / retract bookkeeping
|
|
// MARK: - Issue / retract bookkeeping
|
|
|
|
|
|
|
|
/// Called only from the evaluation queue (forecast + reading paths both
|
|
/// Called only from the evaluation queue (forecast + reading paths both
|
|
@@ -307,6 +341,7 @@ final class GlucoseAlertCoordinator: Injectable {
|
|
|
case .low: typeSlug = "low"
|
|
case .low: typeSlug = "low"
|
|
|
case .forecastedLow: typeSlug = "forecastedLow"
|
|
case .forecastedLow: typeSlug = "forecastedLow"
|
|
|
case .high: typeSlug = "high"
|
|
case .high: typeSlug = "high"
|
|
|
|
|
+ case .carbsRequired: typeSlug = "carbsRequired"
|
|
|
}
|
|
}
|
|
|
return Alert.Identifier(
|
|
return Alert.Identifier(
|
|
|
managerIdentifier: BaseTrioAlertManager.managerIdentifier,
|
|
managerIdentifier: BaseTrioAlertManager.managerIdentifier,
|
|
@@ -335,6 +370,11 @@ final class GlucoseAlertCoordinator: Injectable {
|
|
|
format: String(localized: "Glucose %1$@."),
|
|
format: String(localized: "Glucose %1$@."),
|
|
|
valueString, limitString
|
|
valueString, limitString
|
|
|
)
|
|
)
|
|
|
|
|
+ case .carbsRequired:
|
|
|
|
|
+ return String(
|
|
|
|
|
+ format: String(localized: "To prevent LOW required %d g of carbs"),
|
|
|
|
|
+ Int(NSDecimalNumber(decimal: valueMgDL).intValue)
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -384,6 +424,7 @@ extension GlucoseAlertCoordinator: DeterminationObserver {
|
|
|
func determinationDidUpdate(_ determination: Determination) {
|
|
func determinationDidUpdate(_ determination: Determination) {
|
|
|
evaluationQueue.async { [weak self] in
|
|
evaluationQueue.async { [weak self] in
|
|
|
self?.evaluateForecast(determination)
|
|
self?.evaluateForecast(determination)
|
|
|
|
|
+ self?.evaluateCarbsRequired(determination)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|