Storage+Migrate.swift 73 KB

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