AlarmCondition.swift 3.1 KB

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