AlarmCondition.swift 3.1 KB

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