Alarm.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. //
  2. // Alarm.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-03-15.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. import UserNotifications
  11. enum PlaySoundOption: String, CaseIterable, Codable {
  12. case always, day, night, never
  13. }
  14. enum RepeatSoundOption: String, CaseIterable, Codable {
  15. case always, day, night, never
  16. }
  17. enum ActiveOption: String, CaseIterable, Codable {
  18. case always, day, night
  19. }
  20. struct Alarm: Identifiable, Codable, Equatable {
  21. var id: UUID = UUID()
  22. var type: AlarmType
  23. /// Name of the alarm, defaults to alarm type
  24. var name: String
  25. var isEnabled: Bool = true
  26. /// If the alarm is manually snoozed, we store the end time for the snooze here
  27. var snoozedUntil: Date?
  28. /// Alarm threashold, it can be a bgvalue (in mg/Dl), or day for example
  29. /// Also used as bg limit for drop alarms for example
  30. var threshold: Double?
  31. /// If the alarm looks at predictions, this is how many predictions to include
  32. var predictiveReadings: Int?
  33. /// If the alarm acts on delta, the delta is stored here, it can be a delta bgvalue (in mg/Dl)
  34. /// If a delta alarm is only active below a bg, that bg is stored in threshold
  35. var delta: Double?
  36. /// Number of consecutive 5‑min readings that must satisfy the alarm criteria
  37. var consecutiveReadings: Int?
  38. /// Size of window to observe values, for example battery drop of x within this number of minutes,
  39. var monitoringWindow: Int?
  40. var soundFile: SoundFile
  41. /// Snooze duration, it can be minutes, days or hours. Stepping is different per alarm type
  42. var snoozeDuration: Int = 5
  43. /// When the alarm should play it's sound
  44. var playSoundOption: PlaySoundOption = .always
  45. /// When the sound should repeat
  46. var repeatSoundOption: RepeatSoundOption = .always
  47. /// When is the alarm active
  48. var activeOption: ActiveOption = .always
  49. /// For temporary alerts, it will trigger once and then disable itself
  50. var disableAfterFiring: Bool = false
  51. // ─────────────────────────────────────────────────────────────
  52. // Missed‑Bolus‑specific settings
  53. // ─────────────────────────────────────────────────────────────
  54. /// “Prebolus Max Time” (if a bolus comes within this many minutes *before* the carbs, treat it as prebolus)
  55. var missedBolusPrebolusWindow: Int?
  56. /// “Ignore Bolus <= X units” (don’t count any bolus smaller than or equal to this)
  57. var missedBolusIgnoreSmallBolusUnits: Double?
  58. /// “Ignore Under Grams” (if carb entry is under this many grams, skip the alert)
  59. var missedBolusIgnoreUnderGrams: Double?
  60. /// “Ignore Under BG” (if current BG is below this, skip the alert)
  61. var missedBolusIgnoreUnderBG: Double?
  62. // ─────────────────────────────────────────────────────────────
  63. // Bolus‑Count fields ─
  64. // ─────────────────────────────────────────────────────────────
  65. /// trigger when N or more of those boluses occur...
  66. var bolusCountThreshold: Int?
  67. /// ...within this many minutes
  68. var bolusWindowMinutes: Int?
  69. func checkCondition(data: AlarmData) -> Bool {
  70. return false
  71. }
  72. /// Function for when the alarm is triggered.
  73. /// If this alarm, all alarms is disabled or snoozed, then should not be called. This or all alarmd could be muted, then this function will just generate a notification.
  74. func trigger(config : AlarmConfiguration, now: Date) {
  75. LogManager.shared.log(category: .alarm, message: "Alarm triggered: \(type.rawValue)")
  76. var playSound: Bool = true
  77. // Global mute
  78. if let until = config.muteUntil, until > now {
  79. playSound = false
  80. }
  81. // Mute during calls
  82. if !config.audioDuringCalls && isOnPhoneCall() {
  83. playSound = false
  84. }
  85. // Mute this alarm day or night or always
  86. let cal = Calendar.current
  87. let today = cal.startOfDay(for: now)
  88. let dayStart = cal.date(bySettingHour: config.dayStart.hour,
  89. minute: config.dayStart.minute,
  90. second: 0,
  91. of: today)!
  92. let nightStart = cal.date(bySettingHour: config.nightStart.hour,
  93. minute: config.nightStart.minute,
  94. second: 0,
  95. of: today)!
  96. let isNight: Bool
  97. if nightStart >= dayStart {
  98. isNight = (now >= nightStart) || (now < dayStart)
  99. } else {
  100. isNight = (now >= nightStart) && (now < dayStart)
  101. }
  102. let isDay = !isNight
  103. switch playSoundOption {
  104. case .always:
  105. break
  106. case .never:
  107. playSound = false
  108. case .day where !isDay:
  109. playSound = false
  110. case .night where !isNight:
  111. playSound = false
  112. default:
  113. break
  114. }
  115. let shouldRepeat: Bool = {
  116. switch repeatSoundOption {
  117. case .always: return true
  118. case .never: return false
  119. case .day: return isDay
  120. case .night: return isNight
  121. }
  122. }()
  123. UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
  124. let content = UNMutableNotificationContent()
  125. content.title = type.rawValue
  126. content.subtitle += Observable.shared.bgText.value + " "
  127. content.subtitle += Observable.shared.directionText.value + " "
  128. content.subtitle += Observable.shared.deltaText.value
  129. content.categoryIdentifier = "category"
  130. // This is needed to trigger vibrate on watch and phone
  131. // See if we can use .Critcal
  132. // See if we should use this method instead of direct sound player
  133. content.sound = .default
  134. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
  135. let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
  136. UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
  137. let action = UNNotificationAction(identifier: "snooze", title: "Snooze", options: [])
  138. let category = UNNotificationCategory(identifier: "category", actions: [action], intentIdentifiers: [], options: [])
  139. UNUserNotificationCenter.current().setNotificationCategories([category])
  140. /* TODO när vi gör bg alarm sätt timestamp/datum för denna readings tid så vi inte larmar på samma igen, se isBGBased
  141. if snooozedBGReadingTime != nil {
  142. UserDefaultsRepository.snoozedBGReadingTime.value = snooozedBGReadingTime
  143. }
  144. */
  145. if playSound {
  146. AlarmSound.setSoundFile(str: self.soundFile.rawValue)
  147. AlarmSound.play(repeating: shouldRepeat)
  148. }
  149. }
  150. init(type: AlarmType) {
  151. self.type = type
  152. self.name = type.rawValue
  153. switch type {
  154. case .buildExpire:
  155. /// Alert 7 days before the build expires
  156. self.threshold = 7
  157. self.soundFile = .wrongAnswer
  158. self.snoozeDuration = 1
  159. self.repeatSoundOption = .always
  160. case .low:
  161. soundFile = .indeed
  162. case .iob:
  163. soundFile = .alertToneRingtone1
  164. case .bolus:
  165. soundFile = .dholShuffleloop
  166. case .cob:
  167. soundFile = .alertToneRingtone2
  168. case .high:
  169. soundFile = .timeHasCome
  170. case .fastDrop:
  171. soundFile = .bigClockTicking
  172. case .fastRise:
  173. soundFile = .cartoonFailStringsTrumpet
  174. case .missedReading:
  175. soundFile = .cartoonTipToeSneakyWalk
  176. case .notLooping:
  177. soundFile = .sciFiEngineShutDown
  178. case .missedBolus:
  179. soundFile = .dholShuffleloop
  180. case .sensorChange:
  181. soundFile = .wakeUpWillYou
  182. case .pumpChange:
  183. soundFile = .wakeUpWillYou
  184. case .pump:
  185. soundFile = .marimbaDescend
  186. case .battery:
  187. soundFile = .machineCharge
  188. case .batteryDrop:
  189. soundFile = .machineCharge
  190. case .recBolus:
  191. soundFile = .dholShuffleloop
  192. case .overrideStart:
  193. soundFile = .endingReached
  194. case .overrideEnd:
  195. soundFile = .alertToneBusy
  196. case .tempTargetStart:
  197. soundFile = .endingReached
  198. case .tempTargetEnd:
  199. soundFile = .alertToneBusy
  200. }
  201. }
  202. }