Storage+Migrate.swift 75 KB

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