AlarmCondition.swift 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. // LoopFollow
  2. // AlarmCondition.swift
  3. import Foundation
  4. protocol AlarmCondition {
  5. static var type: AlarmType { get }
  6. init()
  7. /// pure, per-alarm logic against `AlarmData`
  8. func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool
  9. /// `true` when `belowBG`/`aboveBG` are this condition's own trigger
  10. /// threshold, checked in `evaluate`; `false` (the default) treats them
  11. /// as activation limits enforced by `passesBGLimits`.
  12. var checksOwnBGLimits: Bool { get }
  13. }
  14. extension AlarmCondition {
  15. var checksOwnBGLimits: Bool { false }
  16. /// Returns `true` when the alarm is allowed to continue evaluating
  17. /// after BG-limit checks; `false` blocks it immediately.
  18. func passesBGLimits(alarm: Alarm, data: AlarmData) -> Bool {
  19. let bgReading = data.bgReadings.last?.sgv
  20. let haveBG = (bgReading ?? 0) > 0
  21. let bgValue = Double(bgReading ?? 0)
  22. // ────────────────────────────────────
  23. // 1. BG-based alarms always need data
  24. // ────────────────────────────────────
  25. if alarm.type.isBGBased && !haveBG { return false }
  26. // ────────────────────────────────────
  27. // 2. Conditions that check their own threshold in `evaluate`
  28. // skip the limit gate (the data requirement above applies
  29. // regardless).
  30. // ────────────────────────────────────
  31. if checksOwnBGLimits { return true }
  32. // ────────────────────────────────────
  33. // 3. No limits? we’re done.
  34. // ────────────────────────────────────
  35. if alarm.belowBG == nil && alarm.aboveBG == nil { return true }
  36. // If we reach here, there *are* limits.
  37. // Non-BG alarms without a reading must fail;
  38. // BG-based alarms already bailed out above.
  39. guard haveBG else { return false }
  40. switch (alarm.belowBG, alarm.aboveBG) {
  41. case let (lo?, hi?):
  42. return lo < hi ? (bgValue <= lo || bgValue >= hi) // fire outside band
  43. : (hi <= bgValue && bgValue <= lo) // fire inside band
  44. case let (lo?, nil):
  45. return bgValue <= lo
  46. case let (nil, hi?):
  47. return bgValue >= hi
  48. default:
  49. return true
  50. }
  51. }
  52. /// applies every global & per-alarm guard exactly once
  53. func shouldFire(alarm: Alarm, data: AlarmData, now: Date, config: AlarmConfiguration) -> Bool {
  54. // master on/off
  55. guard alarm.isEnabled else { return false }
  56. // per-alarm snooze
  57. if let snooze = alarm.snoozedUntil, snooze > now { return false }
  58. if !passesBGLimits(alarm: alarm, data: data) { return false }
  59. // time-of-day guard
  60. let comps = Calendar.current.dateComponents([.hour, .minute], from: now)
  61. let nowMin = (comps.hour! * 60) + comps.minute!
  62. let dStart = config.dayStart.minutesSinceMidnight
  63. let nStart = config.nightStart.minutesSinceMidnight
  64. let isNight = (nowMin < dStart) || (nowMin >= nStart)
  65. switch alarm.activeOption {
  66. case .always:
  67. break
  68. case .day:
  69. // only fire in day
  70. guard !isNight else { return false }
  71. case .night:
  72. // only fire in night
  73. guard isNight else { return false }
  74. }
  75. // finally, run the type-specific logic
  76. return evaluate(alarm: alarm, data: data, now: now)
  77. }
  78. }