Storage+Migrate.swift 73 KB

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