MainViewController.swift 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. //
  2. // FirstViewController.swift
  3. // LoopFollow
  4. //
  5. // Created by Jon Fawcett on 6/1/20.
  6. // Copyright © 2020 Jon Fawcett. All rights reserved.
  7. //
  8. import UIKit
  9. import Charts
  10. import EventKit
  11. import ShareClient
  12. import UserNotifications
  13. import AVFAudio
  14. class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate {
  15. @IBOutlet weak var BGText: UILabel!
  16. @IBOutlet weak var DeltaText: UILabel!
  17. @IBOutlet weak var DirectionText: UILabel!
  18. @IBOutlet weak var BGChart: LineChartView!
  19. @IBOutlet weak var BGChartFull: LineChartView!
  20. @IBOutlet weak var MinAgoText: UILabel!
  21. @IBOutlet weak var infoTable: UITableView!
  22. @IBOutlet weak var Console: UITableViewCell!
  23. @IBOutlet weak var DragBar: UIImageView!
  24. @IBOutlet weak var PredictionLabel: UILabel!
  25. @IBOutlet weak var LoopStatusLabel: UILabel!
  26. @IBOutlet weak var statsPieChart: PieChartView!
  27. @IBOutlet weak var statsLowPercent: UILabel!
  28. @IBOutlet weak var statsInRangePercent: UILabel!
  29. @IBOutlet weak var statsHighPercent: UILabel!
  30. @IBOutlet weak var statsAvgBG: UILabel!
  31. @IBOutlet weak var statsEstA1C: UILabel!
  32. @IBOutlet weak var statsStdDev: UILabel!
  33. @IBOutlet weak var serverText: UILabel!
  34. @IBOutlet weak var statsView: UIView!
  35. @IBOutlet weak var smallGraphHeightConstraint: NSLayoutConstraint!
  36. var refreshScrollView: UIScrollView!
  37. var refreshControl: UIRefreshControl!
  38. let speechSynthesizer = AVSpeechSynthesizer()
  39. var appStateController: AppStateController?
  40. // Variables for BG Charts
  41. public var numPoints: Int = 13
  42. public var linePlotData: [Double] = []
  43. public var linePlotDataTime: [Double] = []
  44. var firstGraphLoad: Bool = true
  45. var firstBasalGraphLoad: Bool = true
  46. var minAgoBG: Double = 0.0
  47. var currentOverride = 1.0
  48. // Vars for NS Pull
  49. var mmol = false as Bool
  50. var urlUser = UserDefaultsRepository.url.value as String
  51. var token = UserDefaultsRepository.token.value as String
  52. var defaults : UserDefaults?
  53. let consoleLogging = true
  54. var timeofLastBGUpdate = 0 as TimeInterval
  55. var currentSage : sageData?
  56. var currentCage : cageData?
  57. var backgroundTask = BackgroundTask()
  58. // Refresh NS Data
  59. var timer = Timer()
  60. // check every 30 Seconds whether new bgvalues should be retrieved
  61. let timeInterval: TimeInterval = 30.0
  62. // Min Ago Timer
  63. var minAgoTimer = Timer()
  64. var minAgoTimeInterval: TimeInterval = 1.0
  65. // Check Alarms Timer
  66. // Don't check within 1 minute of alarm triggering to give the snoozer time to save data
  67. var checkAlarmTimer = Timer()
  68. var checkAlarmInterval: TimeInterval = 60.0
  69. var calTimer = Timer()
  70. var bgTimer = Timer()
  71. var profileTimer = Timer()
  72. var deviceStatusTimer = Timer()
  73. var treatmentsTimer = Timer()
  74. var alarmTimer = Timer()
  75. var calendarTimer = Timer()
  76. var graphNowTimer = Timer()
  77. // Info Table Setup
  78. var infoManager: InfoManager!
  79. var profileManager = ProfileManager()
  80. var bgData: [ShareGlucoseData] = []
  81. var basalProfile: [basalProfileStruct] = []
  82. var basalData: [basalGraphStruct] = []
  83. var basalScheduleData: [basalGraphStruct] = []
  84. var bolusData: [bolusGraphStruct] = []
  85. var smbData: [bolusGraphStruct] = []
  86. var carbData: [carbGraphStruct] = []
  87. var overrideGraphData: [DataStructs.overrideStruct] = []
  88. var predictionData: [ShareGlucoseData] = []
  89. var bgCheckData: [ShareGlucoseData] = []
  90. var suspendGraphData: [DataStructs.timestampOnlyStruct] = []
  91. var resumeGraphData: [DataStructs.timestampOnlyStruct] = []
  92. var sensorStartGraphData: [DataStructs.timestampOnlyStruct] = []
  93. var noteGraphData: [DataStructs.noteStruct] = []
  94. var chartData = LineChartData()
  95. var newBGPulled = false
  96. var lastCalDate: Double = 0
  97. var latestDirectionString = ""
  98. var latestMinAgoString = ""
  99. var latestDeltaString = ""
  100. var latestLoopStatusString = ""
  101. var latestLoopTime: Double = 0
  102. var latestCOB = ""
  103. var latestBasal = ""
  104. var latestPumpVolume: Double = 50.0
  105. var latestIOB = ""
  106. var lastOverrideStartTime: TimeInterval = 0
  107. var lastOverrideEndTime: TimeInterval = 0
  108. var topBG: Float = UserDefaultsRepository.minBGScale.value
  109. var topPredictionBG: Float = UserDefaultsRepository.minBGScale.value
  110. var lastOverrideAlarm: TimeInterval = 0
  111. // share
  112. var bgDataShare: [ShareGlucoseData] = []
  113. var dexShare: ShareClient?;
  114. // calendar setup
  115. let store = EKEventStore()
  116. var snoozeTabItem: UITabBarItem = UITabBarItem()
  117. // Stores the time of the last speech announcement to prevent repeated announcements.
  118. // This is a temporary safeguard until the issue with multiple calls to speakBG is fixed.
  119. var lastSpeechTime: Date?
  120. var autoScrollPauseUntil: Date? = nil
  121. var IsNotLooping = false
  122. override func viewDidLoad() {
  123. super.viewDidLoad()
  124. // Synchronize info types to ensure arrays are the correct size
  125. UserDefaultsRepository.synchronizeInfoTypes()
  126. // Reset deprecated settings
  127. UserDefaultsRepository.debugLog.value = false;
  128. infoTable.rowHeight = 21
  129. infoTable.dataSource = self
  130. infoTable.tableFooterView = UIView(frame: .zero)
  131. infoTable.bounces = false
  132. infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2)
  133. self.infoManager = InfoManager(tableView: infoTable)
  134. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  135. self.view.layoutIfNeeded()
  136. let shareUserName = UserDefaultsRepository.shareUserName.value
  137. let sharePassword = UserDefaultsRepository.sharePassword.value
  138. let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue
  139. dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer )
  140. // setup show/hide small graph and stats
  141. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  142. statsView.isHidden = !UserDefaultsRepository.showStats.value
  143. BGChart.delegate = self
  144. BGChartFull.delegate = self
  145. if UserDefaultsRepository.forceDarkMode.value {
  146. overrideUserInterfaceStyle = .dark
  147. self.tabBarController?.overrideUserInterfaceStyle = .dark
  148. }
  149. // Load the snoozer tab
  150. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  151. snoozer.loadViewIfNeeded()
  152. // Trigger foreground and background functions
  153. let notificationCenter = NotificationCenter.default
  154. notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  155. notificationCenter.addObserver(self, selector: #selector(appCameToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
  156. // Setup the Graph
  157. if firstGraphLoad {
  158. createGraph()
  159. createSmallBGGraph()
  160. }
  161. // setup display for NS vs Dex
  162. showHideNSDetails()
  163. // Load Startup Data
  164. restartAllTimers()
  165. // Set up refreshScrollView for BGText
  166. refreshScrollView = UIScrollView()
  167. refreshScrollView.translatesAutoresizingMaskIntoConstraints = false
  168. refreshScrollView.alwaysBounceVertical = true
  169. view.addSubview(refreshScrollView)
  170. NSLayoutConstraint.activate([
  171. refreshScrollView.leadingAnchor.constraint(equalTo: BGText.leadingAnchor),
  172. refreshScrollView.trailingAnchor.constraint(equalTo: BGText.trailingAnchor),
  173. refreshScrollView.topAnchor.constraint(equalTo: BGText.topAnchor),
  174. refreshScrollView.bottomAnchor.constraint(equalTo: BGText.bottomAnchor)
  175. ])
  176. refreshControl = UIRefreshControl()
  177. refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
  178. refreshScrollView.addSubview(refreshControl)
  179. // Add this line to prevent scrolling in other directions
  180. refreshScrollView.alwaysBounceVertical = true
  181. refreshScrollView.delegate = self
  182. NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: NSNotification.Name("refresh"), object: nil)
  183. }
  184. deinit {
  185. NotificationCenter.default.removeObserver(self, name: NSNotification.Name("refresh"), object: nil)
  186. }
  187. // Clean all timers and start new ones when refreshing
  188. @objc func refresh() {
  189. print("Refreshing")
  190. // Clear prediction for both Loop or OpenAPS
  191. // Check if Loop prediction data exists and clear it if necessary
  192. if !predictionData.isEmpty {
  193. predictionData.removeAll()
  194. updatePredictionGraph()
  195. }
  196. // Check if OpenAPS prediction data exists and clear it if necessary
  197. let openAPSDataIndices = [12, 13, 14, 15]
  198. for dataIndex in openAPSDataIndices {
  199. let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  200. let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  201. if !mainChart.entries.isEmpty || !smallChart.entries.isEmpty {
  202. updatePredictionGraphGeneric(
  203. dataIndex: dataIndex,
  204. predictionData: [],
  205. chartLabel: "",
  206. color: UIColor.systemGray
  207. )
  208. }
  209. }
  210. MinAgoText.text = "Refreshing"
  211. invalidateTimers()
  212. restartAllTimers()
  213. currentCage = nil
  214. currentSage = nil
  215. lastSpeechTime = nil
  216. refreshControl.endRefreshing()
  217. }
  218. // Scroll down BGText when refreshing
  219. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  220. if scrollView == refreshScrollView {
  221. let yOffset = scrollView.contentOffset.y
  222. if yOffset < 0 {
  223. BGText.transform = CGAffineTransform(translationX: 0, y: -yOffset)
  224. } else {
  225. BGText.transform = CGAffineTransform.identity
  226. }
  227. }
  228. }
  229. override func viewWillAppear(_ animated: Bool) {
  230. // set screen lock
  231. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  232. // check the app state
  233. // TODO: move to a function ?
  234. if let appState = self.appStateController {
  235. if appState.chartSettingsChanged {
  236. // can look at settings flags to be more fine tuned
  237. self.updateBGGraphSettings()
  238. if ChartSettingsChangeEnum.smallGraphHeight.rawValue != 0 {
  239. smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value)
  240. self.view.layoutIfNeeded()
  241. }
  242. // reset the app state
  243. appState.chartSettingsChanged = false
  244. appState.chartSettingsChanges = 0
  245. }
  246. if appState.generalSettingsChanged {
  247. // settings for appBadge changed
  248. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.appBadgeChange.rawValue != 0 {
  249. }
  250. // settings for textcolor changed
  251. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.colorBGTextChange.rawValue != 0 {
  252. self.setBGTextColor()
  253. }
  254. // settings for showStats changed
  255. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showStatsChange.rawValue != 0 {
  256. statsView.isHidden = !UserDefaultsRepository.showStats.value
  257. }
  258. // settings for useIFCC changed
  259. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.useIFCCChange.rawValue != 0 {
  260. updateStats()
  261. }
  262. // settings for showSmallGraph changed
  263. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showSmallGraphChange.rawValue != 0 {
  264. BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value
  265. }
  266. if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showDisplayNameChange.rawValue != 0 {
  267. self.updateServerText()
  268. }
  269. // reset the app state
  270. appState.generalSettingsChanged = false
  271. appState.generalSettingsChanges = 0
  272. }
  273. if appState.infoDataSettingsChanged {
  274. self.infoTable.reloadData()
  275. // reset
  276. appState.infoDataSettingsChanged = false
  277. }
  278. // add more processing of the app state
  279. }
  280. }
  281. // Info Table Functions
  282. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  283. guard let infoManager = infoManager else {
  284. return 0
  285. }
  286. return infoManager.numberOfRows()
  287. }
  288. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  289. let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
  290. let values = infoManager.dataForIndexPath(indexPath)
  291. cell.textLabel?.text = values.name
  292. cell.detailTextLabel?.text = values.value
  293. return cell
  294. }
  295. @objc func appMovedToBackground() {
  296. // Allow screen to turn off
  297. UIApplication.shared.isIdleTimerDisabled = false;
  298. // We want to always come back to the home screen
  299. tabBarController?.selectedIndex = 0
  300. // Cancel the current timer and start a fresh background timer using the settings value only if background task is enabled
  301. if UserDefaultsRepository.backgroundRefresh.value {
  302. BackgroundAlertManager.shared.startBackgroundAlert()
  303. backgroundTask.startBackgroundTask()
  304. }
  305. }
  306. @objc func appCameToForeground() {
  307. // reset screenlock state if needed
  308. UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value;
  309. // Cancel the background tasks, start a fresh timer
  310. if UserDefaultsRepository.backgroundRefresh.value {
  311. backgroundTask.stopBackgroundTask()
  312. BackgroundAlertManager.shared.stopBackgroundAlert()
  313. }
  314. restartAllTimers()
  315. checkAndNotifyVersionStatus()
  316. checkAppExpirationStatus()
  317. }
  318. func checkAndNotifyVersionStatus() {
  319. let versionManager = AppVersionManager()
  320. versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
  321. let now = Date()
  322. // Check if the current version is blacklisted, or if there is a newer version available
  323. if isBlacklisted {
  324. let lastBlacklistShown = UserDefaultsRepository.lastBlacklistNotificationShown.value ?? Date.distantPast
  325. if now.timeIntervalSince(lastBlacklistShown) > 86400 { // 24 hours
  326. self.versionAlert(message: "The current version has a critical issue and should be updated as soon as possible.")
  327. UserDefaultsRepository.lastBlacklistNotificationShown.value = now
  328. UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now
  329. }
  330. } else if isNewer {
  331. let lastVersionUpdateShown = UserDefaultsRepository.lastVersionUpdateNotificationShown.value ?? Date.distantPast
  332. if now.timeIntervalSince(lastVersionUpdateShown) > 1209600 { // 2 weeks
  333. self.versionAlert(message: "A new version is available: \(latestVersion ?? "Unknown"). It is recommended to update.")
  334. UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now
  335. }
  336. }
  337. }
  338. }
  339. func versionAlert(title: String = "Update Available", message: String) {
  340. DispatchQueue.main.async {
  341. let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
  342. alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
  343. self.present(alert, animated: true)
  344. }
  345. }
  346. func checkAppExpirationStatus() {
  347. let now = Date()
  348. let expirationDate = BuildDetails.default.calculateExpirationDate()
  349. let weekBeforeExpiration = Calendar.current.date(byAdding: .day, value: -7, to: expirationDate)!
  350. if now >= weekBeforeExpiration {
  351. let lastExpirationShown = UserDefaultsRepository.lastExpirationNotificationShown.value ?? Date.distantPast
  352. if now.timeIntervalSince(lastExpirationShown) > 86400 { // 24 hours
  353. expirationAlert()
  354. UserDefaultsRepository.lastExpirationNotificationShown.value = now
  355. }
  356. }
  357. }
  358. func expirationAlert() {
  359. DispatchQueue.main.async {
  360. let alert = UIAlertController(title: "App Expiration Warning", message: "This app will expire in less than a week. Please rebuild to continue using it.", preferredStyle: .alert)
  361. alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
  362. self.present(alert, animated: true)
  363. }
  364. }
  365. @objc override func viewDidAppear(_ animated: Bool) {
  366. showHideNSDetails()
  367. }
  368. func stringFromTimeInterval(interval: TimeInterval) -> String {
  369. let interval = Int(interval)
  370. let minutes = (interval / 60) % 60
  371. let hours = (interval / 3600)
  372. return String(format: "%02d:%02d", hours, minutes)
  373. }
  374. func showHideNSDetails() {
  375. var isHidden = false
  376. var isEnabled = true
  377. if UserDefaultsRepository.url.value == "" {
  378. isHidden = true
  379. isEnabled = false
  380. }
  381. LoopStatusLabel.isHidden = isHidden
  382. if IsNotLooping {
  383. PredictionLabel.isHidden = true
  384. }
  385. else {
  386. PredictionLabel.isHidden = isHidden
  387. }
  388. infoTable.isHidden = isHidden
  389. if UserDefaultsRepository.hideInfoTable.value {
  390. infoTable.isHidden = true
  391. }
  392. if UserDefaultsRepository.url.value != "" {
  393. isEnabled = true
  394. }
  395. guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
  396. nightscoutTab.isEnabled = isEnabled
  397. }
  398. func updateBadge(val: Int) {
  399. if UserDefaultsRepository.appBadge.value {
  400. let latestBG = String(val)
  401. UIApplication.shared.applicationIconBadgeNumber = Int(Localizer.removePeriodAndCommaForBadge(Localizer.toDisplayUnits(latestBG))) ?? val
  402. } else {
  403. UIApplication.shared.applicationIconBadgeNumber = 0
  404. }
  405. }
  406. func setBGTextColor() {
  407. if bgData.count > 0 {
  408. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  409. let latestBG = bgData[bgData.count - 1].sgv
  410. var color: NSUIColor = NSUIColor.label
  411. if UserDefaultsRepository.colorBGText.value {
  412. if Float(latestBG) >= UserDefaultsRepository.highLine.value {
  413. color = NSUIColor.systemYellow
  414. } else if Float(latestBG) <= UserDefaultsRepository.lowLine.value {
  415. color = NSUIColor.systemRed
  416. } else {
  417. color = NSUIColor.systemGreen
  418. }
  419. }
  420. BGText.textColor = color
  421. snoozer.BGLabel.textColor = color
  422. }
  423. }
  424. func bgDirectionGraphic(_ value:String)->String
  425. {
  426. if value == nil { return "-" }
  427. let //graphics:[String:String]=["Flat":"\u{2192}","DoubleUp":"\u{21C8}","SingleUp":"\u{2191}","FortyFiveUp":"\u{2197}\u{FE0E}","FortyFiveDown":"\u{2198}\u{FE0E}","SingleDown":"\u{2193}","DoubleDown":"\u{21CA}","None":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-"]
  428. graphics:[String:String]=["Flat":"→","DoubleUp":"↑↑","SingleUp":"↑","FortyFiveUp":"↗","FortyFiveDown":"↘︎","SingleDown":"↓","DoubleDown":"↓↓","None":"-","NONE":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-", "": "-"]
  429. return graphics[value]!
  430. }
  431. func writeCalendar() {
  432. if UserDefaultsRepository.debugLog.value {
  433. self.writeDebugLog(value: "Write calendar start")
  434. }
  435. self.store.requestCalendarAccess { (granted, error) in
  436. if !granted {
  437. print("Failed to get calendar access: \(String(describing: error))")
  438. return
  439. }
  440. self.processCalendarUpdates()
  441. }
  442. }
  443. func processCalendarUpdates() {
  444. if UserDefaultsRepository.calendarIdentifier.value == "" { return }
  445. if self.bgData.count < 1 { return }
  446. // This lets us fire the method to write Min Ago entries only once a minute starting after 6 minutes but allows new readings through
  447. if self.lastCalDate == self.bgData[self.bgData.count - 1].date
  448. && (self.calTimer.isValid || (dateTimeUtils.getNowTimeIntervalUTC() - self.lastCalDate) < 360) {
  449. return
  450. }
  451. // Create Event info
  452. var deltaBG = 0 // protect index out of bounds
  453. if self.bgData.count > 1 {
  454. deltaBG = self.bgData[self.bgData.count - 1].sgv - self.bgData[self.bgData.count - 2].sgv as Int
  455. }
  456. let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - self.bgData[self.bgData.count - 1].date) / 60
  457. var deltaString = ""
  458. if deltaBG < 0 {
  459. deltaString = Localizer.toDisplayUnits(String(deltaBG))
  460. }
  461. else
  462. {
  463. deltaString = "+" + Localizer.toDisplayUnits(String(deltaBG))
  464. }
  465. let direction = self.bgDirectionGraphic(self.bgData[self.bgData.count - 1].direction ?? "")
  466. var eventStartDate = Date(timeIntervalSince1970: self.bgData[self.bgData.count - 1].date)
  467. // if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar start date") }
  468. var eventEndDate = eventStartDate.addingTimeInterval(60 * 10)
  469. var eventTitle = UserDefaultsRepository.watchLine1.value
  470. if (UserDefaultsRepository.watchLine2.value.count > 1) {
  471. eventTitle += "\n" + UserDefaultsRepository.watchLine2.value
  472. }
  473. eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv)))
  474. eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction)
  475. eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString)
  476. if self.currentOverride != 1.0 {
  477. let val = Int( self.currentOverride*100)
  478. // let overrideText = String(format:"%f1", self.currentOverride*100)
  479. let text = String(val) + "%"
  480. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: text)
  481. } else {
  482. eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: "")
  483. }
  484. eventTitle = eventTitle.replacingOccurrences(of: "%LOOP%", with: self.latestLoopStatusString)
  485. var minAgo = ""
  486. if deltaTime > 9 {
  487. // write old BG reading and continue pushing out end date to show last entry
  488. minAgo = String(Int(deltaTime)) + " min"
  489. eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60))
  490. }
  491. var cob = "0"
  492. if self.latestCOB != "" {
  493. cob = self.latestCOB
  494. }
  495. var basal = "~"
  496. if self.latestBasal != "" {
  497. basal = self.latestBasal
  498. }
  499. var iob = "0"
  500. if self.latestIOB != "" {
  501. iob = self.latestIOB
  502. }
  503. eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo)
  504. eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: iob)
  505. eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: cob)
  506. eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal)
  507. // Delete Events from last 2 hours and 2 hours in future
  508. var deleteStartDate = Date().addingTimeInterval(-60*60*2)
  509. var deleteEndDate = Date().addingTimeInterval(60*60*2)
  510. // guard solves for some ios upgrades removing the calendar
  511. guard let deleteCalendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) as? EKCalendar else { return }
  512. var predicate2 = self.store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar])
  513. var eVDelete = self.store.events(matching: predicate2) as [EKEvent]?
  514. if eVDelete != nil {
  515. for i in eVDelete! {
  516. do {
  517. try self.store.remove(i, span: EKSpan.thisEvent, commit: true)
  518. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar Delete") }
  519. } catch let error {
  520. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Error - Calendar Delete") }
  521. print(error)
  522. }
  523. }
  524. }
  525. // Write New Event
  526. var event = EKEvent(eventStore: self.store)
  527. event.title = eventTitle
  528. event.startDate = eventStartDate
  529. event.endDate = eventEndDate
  530. event.calendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value)
  531. do {
  532. try self.store.save(event, span: .thisEvent, commit: true)
  533. self.calTimer.invalidate()
  534. self.startCalTimer(time: (60 * 1))
  535. self.lastCalDate = self.bgData[self.bgData.count - 1].date
  536. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Calendar Write: " + eventTitle) }
  537. //UserDefaultsRepository.savedEventID.value = event.eventIdentifier //save event id to access this particular event later
  538. } catch {
  539. print("*** Error storing to the calendar")
  540. // Display error to user
  541. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Error: Calendar Write") }
  542. }
  543. }
  544. func persistentNotification(bgTime: TimeInterval)
  545. {
  546. if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 {
  547. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  548. snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: Localizer.toDisplayUnits(String(latestDeltaString)), minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG")
  549. }
  550. }
  551. func writeDebugLog(value: String) {
  552. DispatchQueue.main.async {
  553. var logText = "\n" + dateTimeUtils.printNow() + " - " + value
  554. print(logText)
  555. guard let debug = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  556. if debug.debugTextView.text.lengthOfBytes(using: .utf8) > 20000 {
  557. debug.debugTextView.text = ""
  558. }
  559. debug.debugTextView.text += logText
  560. }
  561. }
  562. // General Notifications
  563. func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) {
  564. UNUserNotificationCenter.current().delegate = self
  565. let content = UNMutableNotificationContent()
  566. content.title = title
  567. content.subtitle = subtitle
  568. content.body = body
  569. content.categoryIdentifier = "noAction"
  570. content.sound = .default
  571. let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timer, repeats: false)
  572. let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
  573. UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
  574. }
  575. func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
  576. }
  577. // User has scrolled the chart
  578. func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat) {
  579. let isViewingLatestData = abs(BGChart.highestVisibleX - BGChart.chartXMax) < 0.001
  580. if isViewingLatestData {
  581. autoScrollPauseUntil = nil // User is back at the latest data, allow auto-scrolling
  582. } else {
  583. autoScrollPauseUntil = Date().addingTimeInterval(5 * 60) // User is viewing historical data, pause auto-scrolling
  584. }
  585. }
  586. func calculateMaxBgGraphValue() -> Float {
  587. return max(topBG, topPredictionBG)
  588. }
  589. }