Storage+Migrate.swift 69 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601
  1. // LoopFollow
  2. // Storage+Migrate.swift
  3. // Created by Jonas Björkert on 2025-05-26.
  4. import Foundation
  5. extension Storage {
  6. func migrate() {
  7. // Helper: 1-to-1 type -----------------------------------------------------------------
  8. func move<T: AnyConvertible & Equatable>(
  9. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  10. into newValue: StorageValue<T>
  11. ) {
  12. let item = legacy()
  13. guard item.exists else { return }
  14. newValue.value = item.value
  15. item.setNil(key: item.key)
  16. }
  17. // Helper: Float → Double ------------------------------------------------------------
  18. func moveFloatToDouble(
  19. _ legacy: @autoclosure () -> UserDefaultsValue<Float>,
  20. into newValue: StorageValue<Double>
  21. ) {
  22. let item = legacy()
  23. guard item.exists else { return }
  24. newValue.value = Double(item.value)
  25. item.setNil(key: item.key)
  26. }
  27. if !UserDefaultsRepository.backgroundRefresh.value {
  28. Storage.shared.backgroundRefreshType.value = .none
  29. UserDefaultsRepository.backgroundRefresh.value = true
  30. }
  31. // Remove this in a year later than the release of the new Alarms [BEGIN]
  32. let legacyColorBGText = UserDefaultsValue<Bool>(key: "colorBGText", default: true)
  33. if legacyColorBGText.exists {
  34. Storage.shared.colorBGText.value = legacyColorBGText.value
  35. legacyColorBGText.setNil(key: "colorBGText")
  36. }
  37. let legacyAppBadge = UserDefaultsValue<Bool>(key: "appBadge", default: true)
  38. if legacyAppBadge.exists {
  39. Storage.shared.appBadge.value = legacyAppBadge.value
  40. legacyAppBadge.setNil(key: "appBadge")
  41. }
  42. let legacyForceDarkMode = UserDefaultsValue<Bool>(key: "forceDarkMode", default: true)
  43. if legacyForceDarkMode.exists {
  44. Storage.shared.forceDarkMode.value = legacyForceDarkMode.value
  45. legacyForceDarkMode.setNil(key: "forceDarkMode")
  46. }
  47. let legacyShowStats = UserDefaultsValue<Bool>(key: "showStats", default: true)
  48. if legacyShowStats.exists {
  49. Storage.shared.showStats.value = legacyShowStats.value
  50. legacyShowStats.setNil(key: "showStats")
  51. }
  52. let legacyUseIFCC = UserDefaultsValue<Bool>(key: "useIFCC", default: false)
  53. if legacyUseIFCC.exists {
  54. Storage.shared.useIFCC.value = legacyUseIFCC.value
  55. legacyUseIFCC.setNil(key: "useIFCC")
  56. }
  57. let legacyShowSmallGraph = UserDefaultsValue<Bool>(key: "showSmallGraph", default: true)
  58. if legacyShowSmallGraph.exists {
  59. Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value
  60. legacyShowSmallGraph.setNil(key: "showSmallGraph")
  61. }
  62. let legacyScreenlockSwitchState = UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: true)
  63. if legacyScreenlockSwitchState.exists {
  64. Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value
  65. legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState")
  66. }
  67. let legacyShowDisplayName = UserDefaultsValue<Bool>(key: "showDisplayName", default: false)
  68. if legacyShowDisplayName.exists {
  69. Storage.shared.showDisplayName.value = legacyShowDisplayName.value
  70. legacyShowDisplayName.setNil(key: "showDisplayName")
  71. }
  72. let legacySpeakBG = UserDefaultsValue<Bool>(key: "speakBG", default: false)
  73. if legacySpeakBG.exists {
  74. Storage.shared.speakBG.value = legacySpeakBG.value
  75. legacySpeakBG.setNil(key: "speakBG")
  76. }
  77. let legacySpeakBGAlways = UserDefaultsValue<Bool>(key: "speakBGAlways", default: true)
  78. if legacySpeakBGAlways.exists {
  79. Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value
  80. legacySpeakBGAlways.setNil(key: "speakBGAlways")
  81. }
  82. let legacySpeakLowBG = UserDefaultsValue<Bool>(key: "speakLowBG", default: false)
  83. if legacySpeakLowBG.exists {
  84. Storage.shared.speakLowBG.value = legacySpeakLowBG.value
  85. legacySpeakLowBG.setNil(key: "speakLowBG")
  86. }
  87. let legacySpeakProactiveLowBG = UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false)
  88. if legacySpeakProactiveLowBG.exists {
  89. Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value
  90. legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG")
  91. }
  92. let legacySpeakFastDropDelta = UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0)
  93. if legacySpeakFastDropDelta.exists {
  94. Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value)
  95. legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta")
  96. }
  97. let legacySpeakLowBGLimit = UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0)
  98. if legacySpeakLowBGLimit.exists {
  99. Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value)
  100. legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit")
  101. }
  102. let legacySpeakHighBGLimit = UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0)
  103. if legacySpeakHighBGLimit.exists {
  104. Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value)
  105. legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit")
  106. }
  107. let legacySpeakHighBG = UserDefaultsValue<Bool>(key: "speakHighBG", default: false)
  108. if legacySpeakHighBG.exists {
  109. Storage.shared.speakHighBG.value = legacySpeakHighBG.value
  110. legacySpeakHighBG.setNil(key: "speakHighBG")
  111. }
  112. let legacySpeakLanguage = UserDefaultsValue<String>(key: "speakLanguage", default: "en")
  113. if legacySpeakLanguage.exists {
  114. Storage.shared.speakLanguage.value = legacySpeakLanguage.value
  115. legacySpeakLanguage.setNil(key: "speakLanguage")
  116. }
  117. // ── General (done earlier, but safe to repeat) ──
  118. move(UserDefaultsValue<Bool>(key: "colorBGText", default: true), into: Storage.shared.colorBGText)
  119. move(UserDefaultsValue<Bool>(key: "appBadge", default: true), into: appBadge)
  120. move(UserDefaultsValue<Bool>(key: "forceDarkMode", default: false), into: forceDarkMode)
  121. move(UserDefaultsValue<Bool>(key: "showStats", default: true), into: showStats)
  122. move(UserDefaultsValue<Bool>(key: "useIFCC", default: false), into: useIFCC)
  123. move(UserDefaultsValue<Bool>(key: "showSmallGraph", default: true), into: showSmallGraph)
  124. move(UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: false), into: screenlockSwitchState)
  125. move(UserDefaultsValue<Bool>(key: "showDisplayName", default: false), into: showDisplayName)
  126. // ── Speak-BG ──
  127. move(UserDefaultsValue<Bool>(key: "speakBG", default: false), into: speakBG)
  128. move(UserDefaultsValue<Bool>(key: "speakBGAlways", default: true), into: speakBGAlways)
  129. move(UserDefaultsValue<Bool>(key: "speakLowBG", default: false), into: speakLowBG)
  130. move(UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false), into: speakProactiveLowBG)
  131. move(UserDefaultsValue<Bool>(key: "speakHighBG", default: false), into: speakHighBG)
  132. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0), into: speakLowBGLimit)
  133. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0), into: speakHighBGLimit)
  134. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0), into: speakFastDropDelta)
  135. move(UserDefaultsValue<String>(key: "speakLanguage", default: "en"), into: speakLanguage)
  136. // ── Graph ──
  137. move(UserDefaultsValue<Bool>(key: "showDots", default: true), into: showDots)
  138. move(UserDefaultsValue<Bool>(key: "showLines", default: true), into: showLines)
  139. move(UserDefaultsValue<Bool>(key: "showValues", default: true), into: showValues)
  140. move(UserDefaultsValue<Bool>(key: "showAbsorption", default: true), into: showAbsorption)
  141. move(UserDefaultsValue<Bool>(key: "showDIAMarkers", default: true), into: showDIALines)
  142. move(UserDefaultsValue<Bool>(key: "show30MinLine", default: false), into: show30MinLine)
  143. move(UserDefaultsValue<Bool>(key: "show90MinLine", default: false), into: show90MinLine)
  144. move(UserDefaultsValue<Bool>(key: "showMidnightMarkers", default: false), into: showMidnightLines)
  145. move(UserDefaultsValue<Bool>(key: "smallGraphTreatments", default: true), into: smallGraphTreatments)
  146. move(UserDefaultsValue<Int>(key: "smallGraphHeight", default: 40), into: smallGraphHeight)
  147. move(UserDefaultsValue<Double>(key: "predictionToLoad", default: 1.0), into: predictionToLoad)
  148. move(UserDefaultsValue<Double>(key: "minBasalScale", default: 5.0), into: minBasalScale)
  149. moveFloatToDouble(UserDefaultsValue<Float>(key: "minBGScale", default: 250.0), into: minBGScale)
  150. moveFloatToDouble(UserDefaultsValue<Float>(key: "lowLine", default: 70.0), into: lowLine)
  151. moveFloatToDouble(UserDefaultsValue<Float>(key: "highLine", default: 180.0), into: highLine)
  152. move(UserDefaultsValue<Int>(key: "downloadDays", default: 1), into: downloadDays)
  153. // Remove this in a year later than the release of the new Alarms [END]
  154. // ── Watch / Calendar ────────────────────────────────────────────────
  155. move(UserDefaultsValue<Bool>(key: "writeCalendarEvent", default: false), into: writeCalendarEvent)
  156. move(UserDefaultsValue<String>(key: "calendarIdentifier", default: ""), into: calendarIdentifier)
  157. move(UserDefaultsValue<String>(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1)
  158. move(UserDefaultsValue<String>(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2)
  159. // Migration of generic alarm settings
  160. // ── AlarmConfiguration migration ─────────────────────────────────────────
  161. do {
  162. // Work on a mutable copy, then write the whole thing back once.
  163. var cfg = Storage.shared.alarmConfiguration.value
  164. let cal = Calendar.current
  165. /// Copy *one* legacy value → struct field → delete old key
  166. func move<T: AnyConvertible & Equatable>(
  167. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  168. write: (inout AlarmConfiguration, T) -> Void
  169. ) {
  170. let item = legacy()
  171. guard item.exists else { return }
  172. write(&cfg, item.value)
  173. item.setNil(key: item.key)
  174. }
  175. // 1. Override-volume toggle
  176. move(UserDefaultsValue<Bool>(key: "overrideSystemOutputVolume",
  177. default: cfg.overrideSystemOutputVolume))
  178. {
  179. $0.overrideSystemOutputVolume = $1
  180. }
  181. // 2. Forced output volume itself.
  182. // Prefer newer key (“forcedOutputVolume”); otherwise fall back.
  183. if UserDefaultsValue<Float>(key: "forcedOutputVolume",
  184. default: cfg.forcedOutputVolume).exists
  185. {
  186. move(UserDefaultsValue<Float>(key: "forcedOutputVolume",
  187. default: cfg.forcedOutputVolume))
  188. {
  189. $0.forcedOutputVolume = $1
  190. }
  191. } else {
  192. move(UserDefaultsValue<Float>(key: "systemOutputVolume",
  193. default: cfg.forcedOutputVolume))
  194. {
  195. $0.forcedOutputVolume = $1
  196. }
  197. }
  198. // 3. Play audio during phone calls
  199. move(UserDefaultsValue<Bool>(key: "alertAudioDuringPhone",
  200. default: cfg.audioDuringCalls))
  201. {
  202. $0.audioDuringCalls = $1
  203. }
  204. // 4. Auto-snooze CGM-start alarm
  205. move(UserDefaultsValue<Bool>(key: "alertAutoSnoozeCGMStart",
  206. default: cfg.autoSnoozeCGMStart))
  207. {
  208. $0.autoSnoozeCGMStart = $1
  209. }
  210. // 5. Global “Snooze all” → snoozeUntil
  211. move(UserDefaultsValue<Date?>(key: "alertSnoozeAllTime",
  212. default: cfg.snoozeUntil))
  213. {
  214. $0.snoozeUntil = $1
  215. }
  216. // 6. Global “Mute all” → muteUntil
  217. move(UserDefaultsValue<Date?>(key: "alertMuteAllTime",
  218. default: cfg.muteUntil))
  219. {
  220. $0.muteUntil = $1
  221. }
  222. // 7 & 8. Legacy quiet-hours → day/night start
  223. // (only if both dates exist and are on the same “reference” day)
  224. let qStart = UserDefaultsValue<Date?>(key: "quietHourStart", default: nil)
  225. let qEnd = UserDefaultsValue<Date?>(key: "quietHourEnd", default: nil)
  226. if let s = qStart.value, let e = qEnd.value {
  227. let compsStart = cal.dateComponents([.hour, .minute], from: s)
  228. let compsEnd = cal.dateComponents([.hour, .minute], from: e)
  229. if let sh = compsStart.hour, let sm = compsStart.minute,
  230. let eh = compsEnd.hour, let em = compsEnd.minute
  231. {
  232. cfg.nightStart = TimeOfDay(hour: sh, minute: sm)
  233. cfg.dayStart = TimeOfDay(hour: eh, minute: em)
  234. qStart.setNil(key: qStart.key)
  235. qEnd.setNil(key: qEnd.key)
  236. }
  237. }
  238. // 9. Legacy “ignore zero BG” flag → ignoreZeroBG
  239. move(UserDefaultsValue<Bool>(key: "alertIgnoreZero",
  240. default: cfg.ignoreZeroBG))
  241. {
  242. $0.ignoreZeroBG = $1
  243. }
  244. // finally persist the whole struct
  245. Storage.shared.alarmConfiguration.value = cfg
  246. }
  247. migrateUrgentLowAlarm()
  248. migrateLowAlarm()
  249. migrateHighAlarm()
  250. migrateUrgentHighAlarm()
  251. migrateFastDropAlarm()
  252. migrateFastRiseAlarm()
  253. migrateMissedReadingAlarm()
  254. migrateNotLoopingAlarm()
  255. migrateMissedBolusAlarm()
  256. migrateSensorChangeAlarm()
  257. migratePumpChangeAlarm()
  258. migrateOverrideStartAlarm()
  259. migrateOverrideEndAlarm()
  260. migrateTempTargetStartAlarm()
  261. migrateTempTargetEndAlarm()
  262. migrateTemporaryBGAlarm()
  263. migratePumpVolumeAlarm()
  264. migrateIOBAlarm()
  265. migrateCOBAlarm()
  266. migrateBatteryAlarm()
  267. migrateBatteryDropAlarm()
  268. migrateRecBolusAlarm()
  269. }
  270. // MARK: - One-off alarm migrations
  271. /// Reads *all* `alertUrgentLow*` keys, converts them into a single `Alarm`,
  272. /// saves it to the modern `[Alarm]` store, then deletes the legacy keys.
  273. private func migrateUrgentLowAlarm() {
  274. // Did the user ever change that alert? (No key ⇒ nothing to do.)
  275. guard UserDefaultsValue<Bool>(key: "alertUrgentLowActive", default: false).exists else { return }
  276. /// Helper: fetch-then-delete a legacy value in one line.
  277. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  278. let box = UserDefaultsValue<V>(key: key, default: def)
  279. defer { box.setNil(key: key) }
  280. return box.value
  281. }
  282. // Build the new Alarm ------------------------------------------------
  283. var alarm = Alarm(type: .low)
  284. alarm.name = "Urgent Low"
  285. alarm.isEnabled = take("alertUrgentLowActive", default: false)
  286. alarm.belowBG = Double(take("alertUrgentLowBG", default: 55.0))
  287. alarm.predictiveMinutes = take("alertUrgentLowPredictiveMinutes", default: 0)
  288. alarm.snoozeDuration = take("alertUrgentLowSnooze", default: 5)
  289. alarm.snoozedUntil = take("alertUrgentLowSnoozedTime", default: nil as Date?)
  290. alarm.soundFile = SoundFile(
  291. rawValue: take("alertUrgentLowSound",
  292. default: "Emergency_Alarm_Siren"))
  293. ?? .emergencyAlarmSiren
  294. alarm.playSoundOption = PlaySoundOption(
  295. rawValue: take("alertUrgentLowAudible",
  296. default: "Always").lowercased()) ?? .always
  297. alarm.repeatSoundOption = RepeatSoundOption(
  298. rawValue: take("alertUrgentLowRepeat",
  299. default: "Always").lowercased()) ?? .always
  300. // Day / Night active-window (“Pre-Snooze”)
  301. let autoStr = take("alertUrgentLowAutosnooze", default: "Never").lowercased()
  302. let dayFlag = take("alertUrgentLowAutosnoozeDay", default: false)
  303. let nightFlag = take("alertUrgentLowAutosnoozeNight", default: false)
  304. alarm.activeOption = {
  305. if dayFlag, !nightFlag { return .day }
  306. if !dayFlag, nightFlag { return .night }
  307. switch autoStr {
  308. case "day", "at day": return .day
  309. case "night", "at night": return .night
  310. default: return .always // “Never” → always active
  311. }
  312. }()
  313. // Persist -----------------------------------------------------------------
  314. Storage.shared.alarms.value.append(alarm)
  315. }
  316. // MARK: - Low-BG alarm -------------------------------------------------------
  317. private func migrateLowAlarm() {
  318. // Bail if the old keys were never written
  319. guard UserDefaultsValue<Bool>(key: "alertLowActive", default: false).exists else { return }
  320. // tiny helper: fetch value → erase key
  321. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  322. let box = UserDefaultsValue<V>(key: key, default: def)
  323. defer { box.setNil(key: key) }
  324. return box.value
  325. }
  326. // Build the new Alarm ----------------------------------------------------
  327. var alarm = Alarm(type: .low)
  328. alarm.name = "Low"
  329. alarm.isEnabled = take("alertLowActive", default: false)
  330. alarm.belowBG = Double(take("alertLowBG", default: 70.0))
  331. // “Persistent ≥ X min” → `persistentMinutes`
  332. alarm.persistentMinutes = take("alertLowPersistent", default: 0)
  333. // “Persistence max BG drop” -- ignoring this for now
  334. // let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value
  335. // (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG)
  336. _ = Double(take("alertLowPersistenceMax", default: 5.0))
  337. alarm.snoozeDuration = take("alertLowSnooze", default: 5)
  338. alarm.snoozedUntil = take("alertLowSnoozedTime", default: nil as Date?)
  339. alarm.soundFile = SoundFile(
  340. rawValue: take("alertLowSound",
  341. default: "Indeed")) ?? .indeed
  342. alarm.playSoundOption = PlaySoundOption(
  343. rawValue: take("alertLowAudible",
  344. default: "Always").lowercased()) ?? .always
  345. alarm.repeatSoundOption = RepeatSoundOption(
  346. rawValue: take("alertLowRepeat",
  347. default: "Always").lowercased()) ?? .always
  348. // activeOption ← legacy “Pre-Snooze” flags / picker
  349. let autoStr = take("alertLowAutosnooze", default: "Never").lowercased()
  350. let dayFlag = take("alertLowAutosnoozeDay", default: false)
  351. let nightFlag = take("alertLowAutosnoozeNight", default: false)
  352. alarm.activeOption = {
  353. if dayFlag, !nightFlag { return .day }
  354. if !dayFlag, nightFlag { return .night }
  355. switch autoStr {
  356. case "day", "at day": return .day
  357. case "night", "at night": return .night
  358. default: return .always
  359. }
  360. }()
  361. // Done → append to the modern store
  362. Storage.shared.alarms.value.append(alarm)
  363. }
  364. // MARK: - High-BG alarm -----------------------------------------------------
  365. private func migrateHighAlarm() {
  366. // Only run if the legacy key ever existed
  367. guard UserDefaultsValue<Bool>(key: "alertHighActive", default: false).exists else { return }
  368. /// Fetch → erase helper
  369. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  370. let box = UserDefaultsValue<V>(key: key, default: def)
  371. defer { box.setNil(key: key) } // remove legacy value
  372. return box.value
  373. }
  374. // ---------- Build Alarm -----------------------------------------------
  375. var alarm = Alarm(type: .high)
  376. alarm.name = "High"
  377. alarm.isEnabled = take("alertHighActive", default: false)
  378. alarm.aboveBG = Double(take("alertHighBG", default: 180.0))
  379. alarm.persistentMinutes = take("alertHighPersistent", default: 60)
  380. alarm.snoozeDuration = take("alertHighSnooze", default: 60)
  381. alarm.snoozedUntil = take("alertHighSnoozedTime", default: nil as Date?)
  382. alarm.soundFile = SoundFile(
  383. rawValue: take("alertHighSound",
  384. default: "Time_Has_Come")) ?? .timeHasCome
  385. alarm.playSoundOption = PlaySoundOption(
  386. rawValue: take("alertHighAudible",
  387. default: "Always").lowercased()) ?? .always
  388. alarm.repeatSoundOption = RepeatSoundOption(
  389. rawValue: take("alertHighRepeat",
  390. default: "Always").lowercased()) ?? .always
  391. // ── activeOption derived from “Pre-Snooze” picker & flags
  392. let autoStr = take("alertHighAutosnooze", default: "Never").lowercased()
  393. let dayFlag = take("alertHighAutosnoozeDay", default: false)
  394. let nightFlag = take("alertHighAutosnoozeNight", default: false)
  395. alarm.activeOption = {
  396. if dayFlag, !nightFlag { return .day }
  397. if !dayFlag, nightFlag { return .night }
  398. switch autoStr {
  399. case "day", "at day": return .day
  400. case "night", "at night": return .night
  401. default: return .always
  402. }
  403. }()
  404. // ---------- Persist & we’re done --------------------------------------
  405. Storage.shared.alarms.value.append(alarm)
  406. }
  407. // MARK: - Urgent-High alarm --------------------------------------------------
  408. private func migrateUrgentHighAlarm() {
  409. // run only once, only if the user ever changed that toggle
  410. guard UserDefaultsValue<Bool>(key: "alertUrgentHighActive", default: false).exists else { return }
  411. // helper: read-then-erase a legacy value
  412. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  413. let box = UserDefaultsValue<V>(key: key, default: def)
  414. defer { box.setNil(key: key) } // wipe legacy key
  415. return box.value
  416. }
  417. // ───────── Build the Alarm ────────────────────────────────────────────
  418. var alarm = Alarm(type: .high) // we map to the existing `.high` type
  419. alarm.name = "Urgent High"
  420. alarm.isEnabled = take("alertUrgentHighActive", default: false)
  421. alarm.aboveBG = Double(take("alertUrgentHighBG", default: 250.0))
  422. alarm.snoozeDuration = take("alertUrgentHighSnooze", default: 30)
  423. alarm.snoozedUntil = take("alertUrgentHighSnoozedTime", default: nil as Date?)
  424. alarm.soundFile = SoundFile(
  425. rawValue: take("alertUrgentHighSound",
  426. default: "Pager_Beeps")) ?? .pagerBeeps
  427. alarm.playSoundOption = PlaySoundOption(
  428. rawValue: take("alertUrgentHighAudible",
  429. default: "Always").lowercased()) ?? .always
  430. alarm.repeatSoundOption = RepeatSoundOption(
  431. rawValue: take("alertUrgentHighRepeat",
  432. default: "Always").lowercased()) ?? .always
  433. // activeOption comes from the old “Pre-Snooze” picker + its day/night flags
  434. let autoStr = take("alertUrgentHighAutosnooze", default: "Never").lowercased()
  435. let dayFlag = take("alertUrgentHighAutosnoozeDay", default: false)
  436. let nightFlag = take("alertUrgentHighAutosnoozeNight", default: false)
  437. alarm.activeOption = {
  438. if dayFlag, !nightFlag { return .day }
  439. if !dayFlag, nightFlag { return .night }
  440. switch autoStr { // fall back to picker value
  441. case "day", "at day": return .day
  442. case "night", "at night": return .night
  443. default: return .always
  444. }
  445. }()
  446. // ───────── Persist in new storage ─────────────────────────────────────
  447. Storage.shared.alarms.value.append(alarm)
  448. }
  449. // MARK: - Fast-Drop alarm ----------------------------------------------------
  450. private func migrateFastDropAlarm() {
  451. guard UserDefaultsValue<Bool>(key: "alertFastDropDeltaActive",
  452. default: false).exists else { return }
  453. // helper: read-then-erase
  454. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  455. let box = UserDefaultsValue<V>(key: k, default: d)
  456. defer { box.setNil(key: k) }
  457. return box.value
  458. }
  459. var alarm = Alarm(type: .fastDrop)
  460. alarm.name = "Fast Drop"
  461. alarm.isEnabled = take("alertFastDropDeltaActive", default: false)
  462. // core trigger parameters
  463. alarm.delta = Double(take("alertFastDropDelta", default: 10.0))
  464. alarm.monitoringWindow = take("alertFastDropReadings", default: 3) // store #readings
  465. if take("alertFastDropUseLimit", default: false) {
  466. alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0))
  467. }
  468. // snoozing
  469. alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10)
  470. alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?)
  471. // sound + options
  472. alarm.soundFile = SoundFile(
  473. rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking")
  474. ) ?? .bigClockTicking
  475. alarm.playSoundOption = PlaySoundOption(
  476. rawValue: take("alertFastDropAudible", default: "Always").lowercased()
  477. ) ?? .always
  478. alarm.repeatSoundOption = RepeatSoundOption(
  479. rawValue: take("alertFastDropRepeat", default: "Never").lowercased()
  480. ) ?? .never
  481. // activeOption from old “Pre-Snooze” picker + day/night flags
  482. let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased()
  483. let dayFlag = take("alertFastDropAutosnoozeDay", default: false)
  484. let nightFlag = take("alertFastDropAutosnoozeNight", default: false)
  485. alarm.activeOption = {
  486. if dayFlag, !nightFlag { return .day }
  487. if !dayFlag, nightFlag { return .night }
  488. switch autoStr {
  489. case "day", "at day": return .day
  490. case "night", "at night": return .night
  491. default: return .always
  492. }
  493. }()
  494. Storage.shared.alarms.value.append(alarm)
  495. }
  496. // MARK: - Fast-Rise alarm ----------------------------------------------------
  497. private func migrateFastRiseAlarm() {
  498. guard UserDefaultsValue<Bool>(key: "alertFastRiseDeltaActive",
  499. default: false).exists else { return }
  500. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  501. let box = UserDefaultsValue<V>(key: k, default: d)
  502. defer { box.setNil(key: k) }
  503. return box.value
  504. }
  505. var alarm = Alarm(type: .fastRise)
  506. alarm.name = "Fast Rise"
  507. alarm.isEnabled = take("alertFastRiseDeltaActive", default: false)
  508. alarm.delta = Double(take("alertFastRiseDelta", default: 10.0))
  509. alarm.monitoringWindow = take("alertFastRiseReadings", default: 3)
  510. if take("alertFastRiseUseLimit", default: false) {
  511. alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0))
  512. }
  513. alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10)
  514. alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?)
  515. alarm.soundFile = SoundFile(
  516. rawValue: take("alertFastRiseSound",
  517. default: "Cartoon_Fail_Strings_Trumpet")
  518. ) ?? .cartoonFailStringsTrumpet
  519. alarm.playSoundOption = PlaySoundOption(
  520. rawValue: take("alertFastRiseAudible", default: "Always").lowercased()
  521. ) ?? .always
  522. alarm.repeatSoundOption = RepeatSoundOption(
  523. rawValue: take("alertFastRiseRepeat", default: "Never").lowercased()
  524. ) ?? .never
  525. let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased()
  526. let dayFlag = take("alertFastRiseAutosnoozeDay", default: false)
  527. let nightFlag = take("alertFastRiseAutosnoozeNight", default: false)
  528. alarm.activeOption = {
  529. if dayFlag, !nightFlag { return .day }
  530. if !dayFlag, nightFlag { return .night }
  531. switch autoStr {
  532. case "day", "at day": return .day
  533. case "night", "at night": return .night
  534. default: return .always
  535. }
  536. }()
  537. Storage.shared.alarms.value.append(alarm)
  538. }
  539. // MARK: - Missed-Reading alarm ---------------------------------------------
  540. private func migrateMissedReadingAlarm() {
  541. // Run only when the user has ever modified those settings
  542. guard UserDefaultsValue<Bool>(key: "alertMissedReadingActive",
  543. default: false).exists else { return }
  544. // read-then-erase helper
  545. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  546. let b = UserDefaultsValue<V>(key: k, default: d)
  547. defer { b.setNil(key: k) }
  548. return b.value
  549. }
  550. var alarm = Alarm(type: .missedReading)
  551. alarm.name = "Missed Reading"
  552. alarm.isEnabled = take("alertMissedReadingActive", default: false)
  553. // “No CGM data for X minutes”
  554. alarm.persistentMinutes = take("alertMissedReading", default: 31)
  555. // snoozing
  556. alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30)
  557. alarm.snoozedUntil = take("alertMissedReadingSnoozedTime",
  558. default: nil as Date?)
  559. // (legacy “is-snoozed” flag is implicit in snoozedUntil)
  560. // sound
  561. alarm.soundFile = SoundFile(
  562. rawValue: take("alertMissedReadingSound",
  563. default: "Cartoon_Tip_Toe_Sneaky_Walk")
  564. ) ?? .cartoonTipToeSneakyWalk
  565. // play / repeat options
  566. alarm.playSoundOption = PlaySoundOption(
  567. rawValue: take("alertMissedReadingAudible",
  568. default: "Always").lowercased()
  569. ) ?? .always
  570. alarm.repeatSoundOption = RepeatSoundOption(
  571. rawValue: take("alertMissedReadingRepeat",
  572. default: "Never").lowercased()
  573. ) ?? .never
  574. // activeOption ← “Pre-Snooze” picker + day/night flags
  575. let autoStr = take("alertMissedReadingAutosnooze", default: "Never")
  576. .lowercased()
  577. let dayFlag = take("alertMissedReadingAutosnoozeDay", default: false)
  578. let nightFlag = take("alertMissedReadingAutosnoozeNight", default: false)
  579. alarm.activeOption = {
  580. if dayFlag, !nightFlag { return .day }
  581. if !dayFlag, nightFlag { return .night }
  582. switch autoStr {
  583. case "day", "at day": return .day
  584. case "night", "at night": return .night
  585. default: return .always // “Never” → always on
  586. }
  587. }()
  588. // store in the new world
  589. Storage.shared.alarms.value.append(alarm)
  590. }
  591. // MARK: - “Not Looping” (legacy → Storage.shared.alarms)
  592. private func migrateNotLoopingAlarm() {
  593. // Check if the user ever configured this alarm
  594. let activeFlag = UserDefaultsValue<Bool>(key: "alertNotLoopingActive", default: false)
  595. guard activeFlag.exists else { return } // nothing to migrate
  596. // Convenience: read-then-erase
  597. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  598. let box = UserDefaultsValue<V>(key: key, default: def)
  599. defer { box.setNil(key: key) } // wipe after reading
  600. return box.value
  601. }
  602. // Build the new Alarm ---------------------------------------------------
  603. var alarm = Alarm(type: .notLooping)
  604. alarm.name = "Not Looping Alert"
  605. alarm.isEnabled = take("alertNotLoopingActive", default: false)
  606. alarm.threshold = Double(take("alertNotLooping", default: 31)) // minutes
  607. alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30)
  608. alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?)
  609. alarm.soundFile = SoundFile(rawValue:
  610. take("alertNotLoopingSound",
  611. default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown
  612. // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags --------------
  613. let actDay = take("alertNotLoopingAutosnoozeDay", default: false)
  614. let actNight = take("alertNotLoopingAutosnoozeNight", default: false)
  615. alarm.activeOption = {
  616. switch (actDay, actNight) {
  617. case (true, true): return .always
  618. case (true, false): return .day
  619. case (false, true): return .night
  620. default: return .always // “Never” in old UI
  621. }
  622. }()
  623. // ── PLAY-SOUND option ---------------------------------------------------
  624. let playStr = take("alertNotLoopingAudible", default: "Always").lowercased()
  625. let playDay = take("alertNotLoopingDayTimeAudible", default: true)
  626. let playNight = take("alertNotLoopingNightTimeAudible", default: true)
  627. alarm.playSoundOption = {
  628. if !playDay, !playNight { return .never }
  629. else if playDay, playNight { return .always }
  630. else if playDay { return .day }
  631. else { return .night }
  632. }()
  633. // ── REPEAT-SOUND option -------------------------------------------------
  634. let repStr = take("alertNotLoopingRepeat", default: "Never").lowercased()
  635. let repDay = take("alertNotLoopingDayTime", default: false)
  636. let repNight = take("alertNotLoopingNightTime", default: false)
  637. alarm.repeatSoundOption = {
  638. if repDay, repNight { return .always }
  639. else if repDay, !repNight { return .day }
  640. else if repNight, !repDay { return .night }
  641. else { return .never }
  642. }()
  643. // ── BG-limit guard ------------------------------------------------------
  644. if take("alertNotLoopingUseLimits", default: false) {
  645. alarm.belowBG = Double(take("alertNotLoopingLowerLimit", default: 100.0))
  646. alarm.aboveBG = Double(take("alertNotLoopingUpperLimit", default: 160.0))
  647. }
  648. // ── Per-alarm snooze state ---------------------------------------------
  649. if !take("alertNotLoopingIsSnoozed", default: false) {
  650. alarm.snoozedUntil = nil // ignore stored date if flag isn’t set
  651. }
  652. // Persist & finish -------------------------------------------------------
  653. var list = Storage.shared.alarms.value
  654. list.append(alarm)
  655. Storage.shared.alarms.value = list
  656. }
  657. // MARK: – Missed-Bolus alarm -------------------------------------------------
  658. private func migrateMissedBolusAlarm() {
  659. // Was the old alarm ever configured?
  660. let legacyActive = UserDefaultsValue<Bool>(key: "alertMissedBolusActive",
  661. default: false)
  662. guard legacyActive.exists else { return } // nothing to do
  663. // helper: read-then-delete ------------------------------------------------
  664. func take<V: AnyConvertible & Equatable>(_ k: String,
  665. _ def: V) -> V
  666. {
  667. let box = UserDefaultsValue<V>(key: k, default: def)
  668. defer { box.setNil(key: k) } // wipe after reading
  669. return box.value
  670. }
  671. // ────────────────────────────────────────────────────────────────────────
  672. // Build the new Alarm
  673. // ────────────────────────────────────────────────────────────────────────
  674. var alarm = Alarm(type: .missedBolus)
  675. alarm.name = "Missed Bolus Alert"
  676. alarm.isEnabled = take("alertMissedBolusActive", false)
  677. // core timings
  678. alarm.monitoringWindow = take("alertMissedBolus", 10) // delay
  679. alarm.predictiveMinutes = take("alertMissedBolusPrebolus", 20) // pre-bolus window
  680. alarm.snoozeDuration = take("alertMissedBolusSnooze", 10)
  681. // snooze-state
  682. if take("alertMissedBolusIsSnoozed", false) {
  683. alarm.snoozedUntil = take("alertMissedBolusSnoozedTime",
  684. nil as Date?)
  685. }
  686. // carb / bolus filters
  687. alarm.delta = take("alertMissedBolusIgnoreBolus", 0.5)
  688. alarm.threshold = Double(
  689. take("alertMissedBolusLowGrams", 10))
  690. alarm.aboveBG = Double(
  691. take("alertMissedBolusLowGramsBG", 70.0))
  692. // sound & tone
  693. alarm.soundFile = SoundFile(rawValue:
  694. take("alertMissedBolusSound",
  695. "Dhol_Shuffleloop")) ?? .dholShuffleloop
  696. // ── ACTIVE-DURING ← old “Pre-Snooze” flags
  697. let actDay = take("alertMissedBolusAutosnoozeDay", false)
  698. let actNight = take("alertMissedBolusAutosnoozeNight", false)
  699. alarm.activeOption = {
  700. switch (actDay, actNight) {
  701. case (true, true): return .always
  702. case (true, false): return .day
  703. case (false, true): return .night
  704. default: return .always // “Never” → always
  705. }
  706. }()
  707. // ── PLAY-SOUND ← old “PlaySound” picker
  708. let playDay = take("alertMissedBolusDayTimeAudible", true)
  709. let playNight = take("alertMissedBolusNightTimeAudible", true)
  710. alarm.playSoundOption = {
  711. switch (playDay, playNight) {
  712. case (true, true): return .always
  713. case (true, false): return .day
  714. case (false, true): return .night
  715. default: return .never
  716. }
  717. }()
  718. // ── REPEAT-SOUND ← old “Repeat Sound” picker
  719. let repDay = take("alertMissedBolusDayTime", false)
  720. let repNight = take("alertMissedBolusNightTime", false)
  721. alarm.repeatSoundOption = {
  722. switch (repDay, repNight) {
  723. case (true, true): return .always
  724. case (true, false): return .day
  725. case (false, true): return .night
  726. default: return .never
  727. }
  728. }()
  729. // (The deprecated “alertMissedBolusQuiet” key is ignored.)
  730. // ── Store & finish ------------------------------------------------------
  731. var list = Storage.shared.alarms.value
  732. list.append(alarm)
  733. Storage.shared.alarms.value = list
  734. }
  735. // ─────────────────────────────────────────────────────────────────────────────
  736. // MARK: SAGE → .sensorChange
  737. // ─────────────────────────────────────────────────────────────────────────────
  738. private func migrateSensorChangeAlarm() {
  739. // Was the old setting ever stored?
  740. let flag = UserDefaultsValue<Bool>(key: "alertSAGEActive", default: false)
  741. guard flag.exists else { return }
  742. // tiny helper that *reads + wipes* a legacy key
  743. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  744. let b = UserDefaultsValue<V>(key: k, default: def)
  745. defer { b.setNil(key: k) }
  746. return b.value
  747. }
  748. var alarm = Alarm(type: .sensorChange)
  749. alarm.name = "Sensor Change Reminder"
  750. alarm.isEnabled = take("alertSAGEActive", false)
  751. alarm.threshold = Double(take("alertSAGE", 8)) // hours
  752. alarm.snoozeDuration = take("alertSAGESnooze", 2) // hours
  753. if take("alertSAGEIsSnoozed", false) {
  754. alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?)
  755. }
  756. alarm.soundFile = SoundFile(rawValue:
  757. take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou
  758. // ACTIVE (day / night)
  759. let actDay = take("alertSAGEAutosnoozeDay", false)
  760. let actNight = take("alertSAGEAutosnoozeNight", true)
  761. alarm.activeOption = {
  762. switch (actDay, actNight) {
  763. case (true, true): return .always
  764. case (true, false): return .day
  765. case (false, true): return .night
  766. default: return .always
  767. }
  768. }()
  769. // PLAY sound
  770. let playDay = take("alertSAGEDayTimeAudible", true)
  771. let playNight = take("alertSAGENightTimeAudible", true)
  772. alarm.playSoundOption = {
  773. switch (playDay, playNight) {
  774. case (true, true): return .always
  775. case (true, false): return .day
  776. case (false, true): return .night
  777. default: return .never
  778. }
  779. }()
  780. // REPEAT sound
  781. let repDay = take("alertSAGEDayTime", false)
  782. let repNight = take("alertSAGENightTime", false)
  783. alarm.repeatSoundOption = {
  784. switch (repDay, repNight) {
  785. case (true, true): return .always
  786. case (true, false): return .day
  787. case (false, true): return .night
  788. default: return .never
  789. }
  790. }()
  791. // Persist
  792. var list = Storage.shared.alarms.value
  793. list.append(alarm)
  794. Storage.shared.alarms.value = list
  795. }
  796. // ─────────────────────────────────────────────────────────────────────────────
  797. // MARK: CAGE → .pumpChange
  798. // ─────────────────────────────────────────────────────────────────────────────
  799. private func migratePumpChangeAlarm() {
  800. let flag = UserDefaultsValue<Bool>(key: "alertCAGEActive", default: false)
  801. guard flag.exists else { return }
  802. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  803. let b = UserDefaultsValue<V>(key: k, default: def)
  804. defer { b.setNil(key: k) }
  805. return b.value
  806. }
  807. var alarm = Alarm(type: .pumpChange)
  808. alarm.name = "Pump / Cannula Change"
  809. alarm.isEnabled = take("alertCAGEActive", false)
  810. alarm.threshold = Double(take("alertCAGE", 4)) // hours
  811. alarm.snoozeDuration = take("alertCAGESnooze", 2) // hours
  812. if take("alertCAGEIsSnoozed", false) {
  813. alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?)
  814. }
  815. alarm.soundFile = SoundFile(rawValue:
  816. take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou
  817. // ACTIVE
  818. let actDay = take("alertCAGEAutosnoozeDay", false)
  819. let actNight = take("alertCAGEAutosnoozeNight", true)
  820. alarm.activeOption = {
  821. switch (actDay, actNight) {
  822. case (true, true): return .always
  823. case (true, false): return .day
  824. case (false, true): return .night
  825. default: return .always
  826. }
  827. }()
  828. // PLAY
  829. let playDay = take("alertCAGEDayTimeAudible", true)
  830. let playNight = take("alertCAGENightTimeAudible", true)
  831. alarm.playSoundOption = {
  832. switch (playDay, playNight) {
  833. case (true, true): return .always
  834. case (true, false): return .day
  835. case (false, true): return .night
  836. default: return .never
  837. }
  838. }()
  839. // REPEAT
  840. let repDay = take("alertCAGEDayTime", false)
  841. let repNight = take("alertCAGENightTime", false)
  842. alarm.repeatSoundOption = {
  843. switch (repDay, repNight) {
  844. case (true, true): return .always
  845. case (true, false): return .day
  846. case (false, true): return .night
  847. default: return .never
  848. }
  849. }()
  850. var list = Storage.shared.alarms.value
  851. list.append(alarm)
  852. Storage.shared.alarms.value = list
  853. }
  854. // ─────────────────────────────────────────────────────────────────────────────
  855. // MARK: Override-Start → .overrideStart
  856. // ─────────────────────────────────────────────────────────────────────────────
  857. private func migrateOverrideStartAlarm() {
  858. let exists = UserDefaultsValue<Bool>(key: "alertOverrideStart", default: false)
  859. guard exists.exists else { return } // user never touched it
  860. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  861. let box = UserDefaultsValue<V>(key: k, default: def)
  862. defer { box.setNil(key: k) } // wipe after reading
  863. return box.value
  864. }
  865. var alarm = Alarm(type: .overrideStart)
  866. alarm.name = "Override Started"
  867. alarm.isEnabled = take("alertOverrideStart", false)
  868. alarm.snoozeDuration = 5 // legacy UI had no stepper
  869. if take("alertOverrideStartIsSnoozed", false) {
  870. alarm.snoozedUntil = take("alertOverrideStartSnoozedTime", nil as Date?)
  871. }
  872. alarm.soundFile = SoundFile(
  873. rawValue: take("alertOverrideStartSound", "Ending_Reached")
  874. ) ?? .endingReached
  875. // ── ACTIVE (legacy “Pre-Snooze” day/night flags) ──────────────
  876. let actDay = take("alertOverrideStartAutosnoozeDay", false)
  877. let actNight = take("alertOverrideStartAutosnoozeNight", false)
  878. alarm.activeOption = {
  879. switch (actDay, actNight) {
  880. case (true, true): return .always
  881. case (true, false): return .day
  882. case (false, true): return .night
  883. default: return .always
  884. }
  885. }()
  886. // ── PLAY (legacy “Play Sound” day/night flags) ───────────────
  887. let playDay = take("alertOverrideStartDayTimeAudible", true)
  888. let playNight = take("alertOverrideStartNightTimeAudible", true)
  889. alarm.playSoundOption = {
  890. switch (playDay, playNight) {
  891. case (true, true): return .always
  892. case (true, false): return .day
  893. case (false, true): return .night
  894. default: return .never
  895. }
  896. }()
  897. // ── REPEAT (legacy “Repeat Sound” day/night flags) ───────────
  898. let repDay = take("alertOverrideStartDayTime", false)
  899. let repNight = take("alertOverrideStartNightTime", false)
  900. alarm.repeatSoundOption = {
  901. switch (repDay, repNight) {
  902. case (true, true): return .always
  903. case (true, false): return .day
  904. case (false, true): return .night
  905. default: return .never
  906. }
  907. }()
  908. // ignore & wipe unused keys
  909. _ = take("alertOverrideStartQuiet", false as Bool)
  910. _ = take("alertOverrideStartRepeatAudible", "Always" as String)
  911. var list = Storage.shared.alarms.value
  912. list.append(alarm)
  913. Storage.shared.alarms.value = list
  914. }
  915. // ─────────────────────────────────────────────────────────────────────────────
  916. // MARK: Override-End → .overrideEnd
  917. // ─────────────────────────────────────────────────────────────────────────────
  918. private func migrateOverrideEndAlarm() {
  919. let exists = UserDefaultsValue<Bool>(key: "alertOverrideEnd", default: false)
  920. guard exists.exists else { return }
  921. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  922. let box = UserDefaultsValue<V>(key: k, default: def)
  923. defer { box.setNil(key: k) }
  924. return box.value
  925. }
  926. var alarm = Alarm(type: .overrideEnd)
  927. alarm.name = "Override Ended"
  928. alarm.isEnabled = take("alertOverrideEnd", false)
  929. alarm.snoozeDuration = 5
  930. if take("alertOverrideEndIsSnoozed", false) {
  931. alarm.snoozedUntil = take("alertOverrideEndSnoozedTime", nil as Date?)
  932. }
  933. alarm.soundFile = SoundFile(
  934. rawValue: take("alertOverrideEndSound", "Alert_Tone_Busy")
  935. ) ?? .alertToneBusy
  936. // ACTIVE
  937. let actDay = take("alertOverrideEndAutosnoozeDay", false)
  938. let actNight = take("alertOverrideEndAutosnoozeNight", false)
  939. alarm.activeOption = {
  940. switch (actDay, actNight) {
  941. case (true, true): return .always
  942. case (true, false): return .day
  943. case (false, true): return .night
  944. default: return .always
  945. }
  946. }()
  947. // PLAY
  948. let playDay = take("alertOverrideEndDayTimeAudible", true)
  949. let playNight = take("alertOverrideEndNightTimeAudible", true)
  950. alarm.playSoundOption = {
  951. switch (playDay, playNight) {
  952. case (true, true): return .always
  953. case (true, false): return .day
  954. case (false, true): return .night
  955. default: return .never
  956. }
  957. }()
  958. // REPEAT
  959. let repDay = take("alertOverrideEndDayTime", false)
  960. let repNight = take("alertOverrideEndNightTime", false)
  961. alarm.repeatSoundOption = {
  962. switch (repDay, repNight) {
  963. case (true, true): return .always
  964. case (true, false): return .day
  965. case (false, true): return .night
  966. default: return .never
  967. }
  968. }()
  969. // wipe unused keys
  970. _ = take("alertOverrideEndQuiet", false as Bool)
  971. _ = take("alertOverrideEndRepeatAudible", "Always" as String)
  972. var list = Storage.shared.alarms.value
  973. list.append(alarm)
  974. Storage.shared.alarms.value = list
  975. }
  976. // MARK: ––––– Temp-Target START → .tempTargetStart –––––
  977. private func migrateTempTargetStartAlarm() {
  978. let touched = UserDefaultsValue<Bool>(key: "alertTempTargetStart", default: false)
  979. guard touched.exists else { return }
  980. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  981. let box = UserDefaultsValue<V>(key: k, default: def)
  982. defer { box.setNil(key: k) } // scrub after read
  983. return box.value
  984. }
  985. var alarm = Alarm(type: .tempTargetStart)
  986. alarm.name = "Temp Target Started"
  987. alarm.isEnabled = take("alertTempTargetStart", false)
  988. alarm.snoozeDuration = 5
  989. if take("alertTempTargetStartIsSnoozed", false) {
  990. alarm.snoozedUntil = take("alertTempTargetStartSnoozedTime", nil as Date?)
  991. }
  992. alarm.soundFile = SoundFile(
  993. rawValue: take("alertTempTargetStartSound", "Ending_Reached")
  994. ) ?? .endingReached
  995. // ACTIVE ← legacy Pre-Snooze day/night flags
  996. alarm.activeOption = {
  997. let d = take("alertTempTargetStartAutosnoozeDay", false)
  998. let n = take("alertTempTargetStartAutosnoozeNight", false)
  999. switch (d, n) { case (true, true): return .always
  1000. case (true, false): return .day
  1001. case (false, true): return .night
  1002. default: return .always }
  1003. }()
  1004. // PLAY
  1005. alarm.playSoundOption = {
  1006. let d = take("alertTempTargetStartDayTimeAudible", true)
  1007. let n = take("alertTempTargetStartNightTimeAudible", true)
  1008. switch (d, n) { case (true, true): return .always
  1009. case (true, false): return .day
  1010. case (false, true): return .night
  1011. default: return .never }
  1012. }()
  1013. // REPEAT
  1014. alarm.repeatSoundOption = {
  1015. let d = take("alertTempTargetStartDayTime", false)
  1016. let n = take("alertTempTargetStartNightTime", false)
  1017. switch (d, n) { case (true, true): return .always
  1018. case (true, false): return .day
  1019. case (false, true): return .night
  1020. default: return .never }
  1021. }()
  1022. // wipe “quiet / RepeatAudible” extras
  1023. _ = take("alertTempTargetStartQuiet", false as Bool)
  1024. _ = take("alertTempTargetStartRepeatAudible", "Always" as String)
  1025. var list = Storage.shared.alarms.value
  1026. list.append(alarm)
  1027. Storage.shared.alarms.value = list
  1028. }
  1029. // MARK: ––––– Temp-Target END → .tempTargetEnd –––––
  1030. private func migrateTempTargetEndAlarm() {
  1031. let touched = UserDefaultsValue<Bool>(key: "alertTempTargetEnd", default: false)
  1032. guard touched.exists else { return }
  1033. func take<V: AnyConvertible & Equatable>(_ k: String, _ def: V) -> V {
  1034. let box = UserDefaultsValue<V>(key: k, default: def)
  1035. defer { box.setNil(key: k) }
  1036. return box.value
  1037. }
  1038. var alarm = Alarm(type: .tempTargetEnd)
  1039. alarm.name = "Temp Target Ended"
  1040. alarm.isEnabled = take("alertTempTargetEnd", false)
  1041. alarm.snoozeDuration = 5
  1042. if take("alertTempTargetEndIsSnoozed", false) {
  1043. alarm.snoozedUntil = take("alertTempTargetEndSnoozedTime", nil as Date?)
  1044. }
  1045. alarm.soundFile = SoundFile(
  1046. rawValue: take("alertTempTargetEndSound", "Alert_Tone_Busy")
  1047. ) ?? .alertToneBusy
  1048. alarm.activeOption = {
  1049. let d = take("alertTempTargetEndAutosnoozeDay", false)
  1050. let n = take("alertTempTargetEndAutosnoozeNight", false)
  1051. switch (d, n) { case (true, true): return .always
  1052. case (true, false): return .day
  1053. case (false, true): return .night
  1054. default: return .always }
  1055. }()
  1056. alarm.playSoundOption = {
  1057. let d = take("alertTempTargetEndDayTimeAudible", true)
  1058. let n = take("alertTempTargetEndNightTimeAudible", true)
  1059. switch (d, n) { case (true, true): return .always
  1060. case (true, false): return .day
  1061. case (false, true): return .night
  1062. default: return .never }
  1063. }()
  1064. alarm.repeatSoundOption = {
  1065. let d = take("alertTempTargetEndDayTime", false)
  1066. let n = take("alertTempTargetEndNightTime", false)
  1067. switch (d, n) { case (true, true): return .always
  1068. case (true, false): return .day
  1069. case (false, true): return .night
  1070. default: return .never }
  1071. }()
  1072. _ = take("alertTempTargetEndQuiet", false as Bool)
  1073. _ = take("alertTempTargetEndRepeatAudible", "Always" as String)
  1074. var list = Storage.shared.alarms.value
  1075. list.append(alarm)
  1076. Storage.shared.alarms.value = list
  1077. }
  1078. // MARK: ––––– TEMPORARY BG LIMIT → .temporary –––––
  1079. private func migrateTemporaryBGAlarm() {
  1080. let flag = UserDefaultsValue<Bool>(key: "alertTemporaryActive", default: false)
  1081. guard flag.exists else { return }
  1082. func take<V: AnyConvertible & Equatable>(_ k: String, _ d: V) -> V {
  1083. let box = UserDefaultsValue<V>(key: k, default: d)
  1084. defer { box.setNil(key: k) }
  1085. return box.value
  1086. }
  1087. var alarm = Alarm(type: .temporary)
  1088. alarm.name = "Temporary BG Limit"
  1089. alarm.isEnabled = take("alertTemporaryActive", false)
  1090. // limit direction ↓ / ↑
  1091. let limit = Double(take("alertTemporaryBG", 90.0 as Float))
  1092. if take("alertTemporaryBelow", true) {
  1093. alarm.belowBG = limit
  1094. } else {
  1095. alarm.aboveBG = limit
  1096. }
  1097. // audio & repeat
  1098. alarm.soundFile = SoundFile(rawValue:
  1099. take("alertTemporarySound", "Indeed")) ?? .indeed
  1100. alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never
  1101. alarm.repeatSoundOption = take("alertTemporaryBGRepeat", false) ? .always : .never
  1102. Storage.shared.alarms.value.append(alarm)
  1103. }
  1104. // MARK: ––––– PUMP RESERVOIR LEVEL → .pump –––––
  1105. private func migratePumpVolumeAlarm() {
  1106. let flag = UserDefaultsValue<Bool>(key: "alertPump", default: false)
  1107. guard flag.exists else { return }
  1108. func take<V: AnyConvertible & Equatable>(_ k: String, _ d: V) -> V {
  1109. let box = UserDefaultsValue<V>(key: k, default: d)
  1110. defer { box.setNil(key: k) }
  1111. return box.value
  1112. }
  1113. var alarm = Alarm(type: .pump)
  1114. alarm.name = "Pump Reservoir"
  1115. alarm.isEnabled = take("alertPump", false)
  1116. alarm.threshold = Double(take("alertPumpAt", 10)) // units left
  1117. // Snooze — stored in hours, so keep as-is.
  1118. alarm.snoozeDuration = take("alertPumpSnoozeHours", 5)
  1119. if take("alertPumpIsSnoozed", false) {
  1120. alarm.snoozedUntil = take("alertPumpSnoozedTime", nil as Date?)
  1121. }
  1122. alarm.soundFile = SoundFile(rawValue:
  1123. take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend
  1124. // PLAY-sound option (day / night / never)
  1125. alarm.playSoundOption = {
  1126. let d = take("alertPumpDayTimeAudible", true)
  1127. let n = take("alertPumpNightTimeAudible", true)
  1128. switch (d, n) {
  1129. case (true, true): return .always
  1130. case (true, false): return .day
  1131. case (false, true): return .night
  1132. default: return .never
  1133. }
  1134. }()
  1135. // REPEAT-sound option – derived from legacy picker flags
  1136. alarm.repeatSoundOption = {
  1137. let d = take("alertPumpDayTime", false)
  1138. let n = take("alertPumpNightTime", false)
  1139. switch (d, n) {
  1140. case (true, true): return .always
  1141. case (true, false): return .day
  1142. case (false, true): return .night
  1143. default: return .never
  1144. }
  1145. }()
  1146. // ACTIVE day/night ← old “Pre-Snooze” flags
  1147. alarm.activeOption = {
  1148. let d = take("alertPumpAutosnoozeDay", false)
  1149. let n = take("alertPumpAutosnoozeNight", false)
  1150. switch (d, n) {
  1151. case (true, true): return .always
  1152. case (true, false): return .day
  1153. case (false, true): return .night
  1154. default: return .always
  1155. }
  1156. }()
  1157. // Discard no-longer-needed extras
  1158. _ = take("alertPumpQuiet", false as Bool)
  1159. _ = take("alertPumpRepeat", "Never" as String)
  1160. _ = take("alertPumpAudible", "Always" as String)
  1161. _ = take("alertPumpAutosnooze", "Never" as String)
  1162. Storage.shared.alarms.value.append(alarm)
  1163. }
  1164. // -----------------------------------------------------------------------------
  1165. // MARK: - Helpers (place them once, near the top of the migrate() file)
  1166. // -----------------------------------------------------------------------------
  1167. /// Legacy picker + day/night flags → PlaySoundOption
  1168. private func playOption(from picker: String,
  1169. dayFlag: Bool,
  1170. nightFlag: Bool) -> PlaySoundOption
  1171. {
  1172. let p = picker.lowercased()
  1173. if p == "never" { return .never }
  1174. if p == "always" { return .always }
  1175. switch (dayFlag, nightFlag) {
  1176. case (true, true): return .always
  1177. case (true, false): return .day
  1178. case (false, true): return .night
  1179. default: return .never
  1180. }
  1181. }
  1182. /// Legacy picker + day/night flags → RepeatSoundOption
  1183. private func repeatOption(from picker: String,
  1184. dayFlag: Bool,
  1185. nightFlag: Bool) -> RepeatSoundOption
  1186. {
  1187. let p = picker.lowercased()
  1188. if p == "never" { return .never }
  1189. if p == "always" { return .always }
  1190. switch (dayFlag, nightFlag) {
  1191. case (true, true): return .always
  1192. case (true, false): return .day
  1193. case (false, true): return .night
  1194. default: return .never
  1195. }
  1196. }
  1197. /// Convenience: load-then-erase a legacy `UserDefaultsValue`
  1198. private func take<V: AnyConvertible & Equatable>(
  1199. _ key: String,
  1200. _ defaultValue: V
  1201. ) -> V {
  1202. let box = UserDefaultsValue<V>(key: key, default: defaultValue)
  1203. defer { box.setNil(key: key) }
  1204. return box.value
  1205. }
  1206. // -----------------------------------------------------------------------------
  1207. // MARK: - IOB Alarm migration
  1208. // -----------------------------------------------------------------------------
  1209. private func migrateIOBAlarm() {
  1210. // Only migrate if the user ever touched IOB alarm settings
  1211. guard UserDefaultsValue<Bool>(key: "alertIOB", default: false).exists else {
  1212. return
  1213. }
  1214. var alarm = Alarm(type: .iob)
  1215. alarm.name = "High IOB"
  1216. alarm.isEnabled = take("alertIOB", false)
  1217. alarm.aboveBG = nil // BG band not used
  1218. alarm.threshold = Double(take("alertIOBAt", 1.5)) // units threshold
  1219. alarm.bolusCountThreshold = take("alertIOBNumber", 3)
  1220. alarm.bolusWindowMinutes = take("alertIOBBolusesWithin", 60)
  1221. alarm.delta = Double(take("alertIOBMaxBoluses", 10)) // max total bolus
  1222. alarm.snoozeDuration = take("alertIOBSnoozeHours", 1) * 60 // hours → minutes
  1223. alarm.snoozedUntil = take("alertIOBSnoozedTime", nil as Date?)
  1224. alarm.soundFile = SoundFile(rawValue:
  1225. take("alertIOBSound", "Alert_Tone_Ringtone_1"))
  1226. ?? .alertToneRingtone1
  1227. // Audio options
  1228. alarm.playSoundOption = playOption(
  1229. from: take("alertIOBAudible", "Always"),
  1230. dayFlag: take("alertIOBDayTimeAudible", true),
  1231. nightFlag: take("alertIOBNightTimeAudible", true)
  1232. )
  1233. alarm.repeatSoundOption = repeatOption(
  1234. from: take("alertIOBRepeat", "Always"),
  1235. dayFlag: take("alertIOBDayTime", true),
  1236. nightFlag: take("alertIOBNightTime", true)
  1237. )
  1238. // Active (day/night) option comes from the legacy “Pre-Snooze” picker,
  1239. // but the IOB alarm never had one, so treat it as always-on.
  1240. alarm.activeOption = .always
  1241. // Persist
  1242. Storage.shared.alarms.value.append(alarm)
  1243. }
  1244. // -----------------------------------------------------------------------------
  1245. // MARK: - COB Alarm migration
  1246. // -----------------------------------------------------------------------------
  1247. private func migrateCOBAlarm() {
  1248. // Only migrate if the user ever touched COB alarm settings
  1249. guard UserDefaultsValue<Bool>(key: "alertCOB", default: false).exists else {
  1250. return
  1251. }
  1252. var alarm = Alarm(type: .cob)
  1253. alarm.name = "High COB"
  1254. alarm.isEnabled = take("alertCOB", false)
  1255. alarm.threshold = Double(take("alertCOBAt", 50)) // grams threshold
  1256. alarm.snoozeDuration = take("alertCOBSnoozeHours", 1) * 60 // hours → minutes
  1257. alarm.snoozedUntil = take("alertCOBSnoozedTime", nil as Date?)
  1258. alarm.soundFile = SoundFile(rawValue:
  1259. take("alertCOBSound", "Alert_Tone_Ringtone_2"))
  1260. ?? .alertToneRingtone2
  1261. // Audio options
  1262. alarm.playSoundOption = playOption(
  1263. from: take("alertCOBAudible", "Always"),
  1264. dayFlag: take("alertCOBDayTimeAudible", true),
  1265. nightFlag: take("alertCOBNightTimeAudible", true)
  1266. )
  1267. alarm.repeatSoundOption = repeatOption(
  1268. from: take("alertCOBRepeat", "Always"),
  1269. dayFlag: take("alertCOBDayTime", true),
  1270. nightFlag: take("alertCOBNightTime", true)
  1271. )
  1272. alarm.activeOption = .always // same reason as above
  1273. // Persist
  1274. Storage.shared.alarms.value.append(alarm)
  1275. }
  1276. // =============================================================================
  1277. // BATTERY-LEVEL alarm (old keys → .battery)
  1278. // =============================================================================
  1279. private func migrateBatteryAlarm() {
  1280. guard UserDefaultsValue<Bool>(key: "alertBatteryActive",
  1281. default: false).exists else { return }
  1282. var alarm = Alarm(type: .battery)
  1283. alarm.name = "Low Battery"
  1284. alarm.isEnabled = take("alertBatteryActive", false)
  1285. alarm.threshold = Double(take("alertBatteryLevel", 25)) // %
  1286. alarm.snoozeDuration = take("alertBatterySnoozeHours", 1) * 60
  1287. alarm.snoozedUntil = take("alertBatterySnoozedTime", nil as Date?)
  1288. alarm.soundFile = SoundFile(rawValue:
  1289. take("alertBatterySound", "Machine_Charge"))
  1290. ?? .machineCharge
  1291. // ── audio – legacy had a simple Bool “repeat / no-repeat”
  1292. let rpt = take("alertBatteryRepeat", true)
  1293. alarm.playSoundOption = .always // no day/night picker in legacy UI
  1294. alarm.repeatSoundOption = rpt ? .always : .never
  1295. alarm.activeOption = .always // no day/night activation picker
  1296. Storage.shared.alarms.value.append(alarm)
  1297. }
  1298. // =============================================================================
  1299. // BATTERY-DROP alarm (old keys → .batteryDrop)
  1300. // =============================================================================
  1301. private func migrateBatteryDropAlarm() {
  1302. guard UserDefaultsValue<Bool>(key: "alertBatteryDropActive",
  1303. default: false).exists else { return }
  1304. var alarm = Alarm(type: .batteryDrop)
  1305. alarm.name = "Battery Drop"
  1306. alarm.isEnabled = take("alertBatteryDropActive", false)
  1307. alarm.delta = Double(take("alertBatteryDropPercentage", 5)) // % drop
  1308. alarm.monitoringWindow = take("alertBatteryDropPeriod", 15) // min
  1309. alarm.snoozeDuration = take("alertBatteryDropSnoozeHours", 1) * 60
  1310. alarm.snoozedUntil = take("alertBatteryDropSnoozedTime", nil as Date?)
  1311. alarm.soundFile = SoundFile(rawValue:
  1312. take("alertBatteryDropSound", "Machine_Charge"))
  1313. ?? .machineCharge
  1314. let rpt = take("alertBatteryDropRepeat", true)
  1315. alarm.playSoundOption = .always
  1316. alarm.repeatSoundOption = rpt ? .always : .never
  1317. alarm.activeOption = .always
  1318. Storage.shared.alarms.value.append(alarm)
  1319. }
  1320. // =============================================================================
  1321. // REC-BOLUS alarm (old keys → .recBolus)
  1322. // =============================================================================
  1323. private func migrateRecBolusAlarm() {
  1324. guard UserDefaultsValue<Bool>(key: "alertRecBolusActive",
  1325. default: false).exists else { return }
  1326. var alarm = Alarm(type: .recBolus)
  1327. alarm.name = "Recommended Bolus"
  1328. alarm.isEnabled = take("alertRecBolusActive", false)
  1329. alarm.delta = take("alertRecBolusLevel", 1.0) // units
  1330. alarm.snoozeDuration = take("alertRecBolusSnooze", 5) // min
  1331. alarm.snoozedUntil = take("alertRecBolusSnoozedTime", nil as Date?)
  1332. alarm.soundFile = SoundFile(rawValue:
  1333. take("alertRecBolusSound", "Dhol_Shuffleloop"))
  1334. ?? .dholShuffleloop
  1335. let rpt = take("alertRecBolusRepeat", false)
  1336. alarm.playSoundOption = .always
  1337. alarm.repeatSoundOption = rpt ? .always : .never
  1338. alarm.activeOption = .always
  1339. Storage.shared.alarms.value.append(alarm)
  1340. }
  1341. }