Storage+Migrate.swift 74 KB

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