Storage+Migrate.swift 72 KB

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