Storage+Migrate.swift 73 KB

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