LowBGCondition.swift 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. // LoopFollow
  2. // LowBGCondition.swift
  3. import Foundation
  4. /// Fires when:
  5. /// • every BG in `persistentMinutes` (if set) **and** the latest BG are ≤ `belowBG`; **or**
  6. /// • any predicted BG within `predictiveMinutes` is ≤ `belowBG`.
  7. struct LowBGCondition: AlarmCondition {
  8. static let type: AlarmType = .low
  9. init() {}
  10. /// `belowBG` is this alarm's trigger threshold, not an activation limit:
  11. /// the predictive look-ahead must be able to run while the latest BG is
  12. /// still above it, so `evaluate` checks the threshold itself.
  13. var checksOwnBGLimits: Bool { true }
  14. func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool {
  15. // ────────────────────────────────
  16. // 0. sanity checks
  17. // ────────────────────────────────
  18. guard let belowBG = alarm.belowBG else { return false }
  19. guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false }
  20. func isLow(_ g: GlucoseValue) -> Bool {
  21. g.sgv > 0 && Double(g.sgv) <= belowBG
  22. }
  23. // ────────────────────────────────
  24. // 1. predictive low?
  25. // ────────────────────────────────
  26. var predictiveTrigger = false
  27. if let predictiveMinutes = alarm.predictiveMinutes,
  28. predictiveMinutes > 0,
  29. !data.predictionData.isEmpty
  30. {
  31. let lookAhead = min(
  32. data.predictionData.count,
  33. Int(ceil(Double(predictiveMinutes) / 5.0))
  34. )
  35. for i in 0 ..< lookAhead where isLow(data.predictionData[i]) {
  36. predictiveTrigger = true
  37. break
  38. }
  39. }
  40. // ────────────────────────────────
  41. // 2. persistent low window (ALL readings must be low)
  42. // ────────────────────────────────
  43. var persistentOK = true
  44. if let persistentMinutes = alarm.persistentMinutes,
  45. persistentMinutes > 0
  46. {
  47. let window = Int(ceil(Double(persistentMinutes) / 5.0))
  48. if data.bgReadings.count >= window {
  49. let recent = data.bgReadings.suffix(window)
  50. persistentOK = recent.allSatisfy(isLow)
  51. } else {
  52. // not enough samples to prove persistence ⇒ don’t alarm yet
  53. persistentOK = false
  54. }
  55. }
  56. // ────────────────────────────────
  57. // 3. rising BG suppression (optional)
  58. // Stay silent while the latest reading is above the previous one;
  59. // only alarm when BG is flat or still falling.
  60. // ────────────────────────────────
  61. if alarm.suppressIfRising, data.bgReadings.count >= 2 {
  62. let last = data.bgReadings[data.bgReadings.count - 1]
  63. let previous = data.bgReadings[data.bgReadings.count - 2]
  64. if last.sgv > 0, previous.sgv > 0, last.sgv > previous.sgv {
  65. return false
  66. }
  67. }
  68. // ────────────────────────────────
  69. // 4. final decision
  70. // ────────────────────────────────
  71. return (isLow(latest) && persistentOK) || predictiveTrigger
  72. }
  73. }