MainViewController.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  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. class MainViewController: UIViewController, UITableViewDataSource {
  11. @IBOutlet weak var BGText: UILabel!
  12. @IBOutlet weak var DeltaText: UILabel!
  13. @IBOutlet weak var DirectionText: UILabel!
  14. @IBOutlet weak var BGChart: LineChartView!
  15. @IBOutlet weak var BGChartFull: LineChartView!
  16. @IBOutlet weak var MinAgoText: UILabel!
  17. @IBOutlet weak var infoTable: UITableView!
  18. //NS BG Struct
  19. struct sgvData: Codable {
  20. var sgv: Int
  21. var date: TimeInterval
  22. var direction: String?
  23. }
  24. // Data Table Struct
  25. struct infoData {
  26. var name: String
  27. var value: String
  28. }
  29. // Variables for BG Charts
  30. public var numPoints: Int = 13
  31. public var linePlotData: [Double] = []
  32. public var linePlotDataTime: [Double] = []
  33. var firstStart: Bool = true
  34. // Vars for NS Pull
  35. var graphHours:Int=24
  36. var mmol = false as Bool
  37. var urlUser = UserDefaultsRepository.url.value as String
  38. var token = "" as String
  39. var defaults : UserDefaults?
  40. let consoleLogging = true
  41. var timeofLastBGUpdate = 0 as TimeInterval
  42. var backgroundTask = BackgroundTask()
  43. // Refresh NS Data
  44. var timer = Timer()
  45. // check every 30 Seconds whether new bgvalues should be retrieved
  46. let timeInterval: TimeInterval = 30.0
  47. // Info Table Setup
  48. var tableData = [
  49. infoData(name: "IOB", value: ""), //0
  50. infoData(name: "COB", value: ""), //1
  51. infoData(name: "Basal", value: ""), //2
  52. infoData(name: "Override", value: ""), //3
  53. infoData(name: "Battery", value: ""), //4
  54. infoData(name: "Pump", value: ""), //5
  55. infoData(name: "Loop", value: ""), //6
  56. infoData(name: "SAGE", value: ""), //7
  57. infoData(name: "CAGE", value: "") //8
  58. ]
  59. override func viewDidLoad() {
  60. super.viewDidLoad()
  61. UNUserNotificationCenter.current().requestAuthorization(options: .badge) { (granted, error) in
  62. if error != nil {
  63. // success!
  64. }
  65. }
  66. // ToDo: Should continue running in background
  67. // stop timer when app enters in background, start is again when becomes active
  68. let notificationCenter = NotificationCenter.default
  69. notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  70. notificationCenter.addObserver(self, selector: #selector(appCameToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
  71. //Bind info data
  72. infoTable.rowHeight = 25
  73. infoTable.dataSource = self
  74. }
  75. override func viewWillAppear(_ animated: Bool) {
  76. Reload()
  77. }
  78. // Info Table Functions
  79. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  80. return tableData.count
  81. }
  82. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  83. let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
  84. let values = tableData[indexPath.row]
  85. cell.textLabel?.text = values.name
  86. cell.detailTextLabel?.text = values.value
  87. return cell
  88. }
  89. // Timer
  90. fileprivate func startTimer() {
  91. timer = Timer.scheduledTimer(timeInterval: timeInterval,
  92. target: self,
  93. selector: #selector(MainViewController.timerDidEnd(_:)),
  94. userInfo: nil,
  95. repeats: true)
  96. }
  97. @objc func appMovedToBackground() {
  98. // timer.invalidate()
  99. backgroundTask.startBackgroundTask()
  100. }
  101. @objc func appCameToForeground() {
  102. backgroundTask.stopBackgroundTask()
  103. //timer.invalidate()
  104. startTimer()
  105. loadBGData(urlUser: urlUser)
  106. loadDeviceStatus(urlUser: urlUser)
  107. }
  108. // check whether new Values should be retrieved
  109. @objc func timerDidEnd(_ timer:Timer) {
  110. print("timer ended")
  111. Reload()
  112. }
  113. func Reload() {
  114. loadBGData(urlUser: urlUser)
  115. clearLastInfoData()
  116. loadDeviceStatus(urlUser: urlUser)
  117. loadTempBasals(urlUser: urlUser)
  118. }
  119. // Main NS Data Pull
  120. func loadBGData(urlUser: String) {
  121. let points = String(self.graphHours * 12 + 1)
  122. var urlBGDataPath: String = urlUser + "/api/v1/entries/sgv.json?"
  123. if token == "" {
  124. urlBGDataPath = urlBGDataPath + "count=" + points
  125. }
  126. else
  127. {
  128. urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
  129. }
  130. guard let urlBGData = URL(string: urlBGDataPath) else {
  131. return
  132. }
  133. var request = URLRequest(url: urlBGData)
  134. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  135. let getBGTask = URLSession.shared.dataTask(with: request) { data, response, error in
  136. if self.consoleLogging == true {print("start bg url")}
  137. guard error == nil else {
  138. return
  139. }
  140. guard let data = data else {
  141. return
  142. }
  143. let decoder = JSONDecoder()
  144. let entriesResponse = try? decoder.decode([sgvData].self, from: data)
  145. if let entriesResponse = entriesResponse {
  146. DispatchQueue.main.async {
  147. if self.backgroundTask.player.isPlaying {
  148. self.updateBadge(entries: entriesResponse)
  149. } else {
  150. self.updateBG(entries: entriesResponse)
  151. self.createGraph(entries: entriesResponse)
  152. }
  153. }
  154. }
  155. else
  156. {
  157. return
  158. }
  159. }
  160. getBGTask.resume()
  161. }
  162. //Clear the info data before next pull
  163. func clearLastInfoData(){
  164. for i in 0..<tableData.count{
  165. tableData[i].value = ""
  166. }
  167. }
  168. // NS Device Status Pull
  169. func loadDeviceStatus(urlUser: String) {
  170. var urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=1"
  171. if token != "" {
  172. urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?token=" + token + "&count=1"
  173. }
  174. let escapedAddress = urlStringDeviceStatus.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  175. guard let urlDeviceStatus = URL(string: escapedAddress!) else {
  176. return
  177. }
  178. if consoleLogging == true {print("entered 2nd task.")}
  179. var requestDeviceStatus = URLRequest(url: urlDeviceStatus)
  180. requestDeviceStatus.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  181. let deviceStatusTask = URLSession.shared.dataTask(with: requestDeviceStatus) { data, response, error in
  182. if self.consoleLogging == true {print("in update loop.")}
  183. guard error == nil else {
  184. return
  185. }
  186. guard let data = data else {
  187. return
  188. }
  189. let json = try? JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]]
  190. if let json = json {
  191. DispatchQueue.main.async {
  192. self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
  193. }
  194. }
  195. else
  196. {
  197. return
  198. }
  199. if self.consoleLogging == true {print("finish pump update")}
  200. }
  201. deviceStatusTask.resume()
  202. }
  203. // Need to figure out the date to pull only last 24 hours
  204. func loadTempBasals(urlUser: String) {
  205. var dayComponent = DateComponents()
  206. dayComponent.day = -1 // For removing one day (yesterday): -1
  207. let theCalendar = Calendar.current
  208. let yesterday = theCalendar.date(byAdding: dayComponent, to: Date())
  209. let dateFormatter = DateFormatter()
  210. dateFormatter.dateFormat = "yyyy-mm-ddTHH:mm:ss"
  211. //dateFormatter.timeZone = NSTimeZone(name: "UTC")
  212. //let date: NSDate? = dateFormatter.dateFromString("2016-02-29 12:24:26")
  213. //print(date)
  214. var urlStringBasal = urlUser + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=2020-06-02T22:46:26"
  215. if token != "" {
  216. urlStringBasal = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=2020-06-02T22:46:26"
  217. }
  218. let escapedAddress = urlStringBasal.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  219. guard let urlBasal = URL(string: escapedAddress!) else {
  220. return
  221. }
  222. if consoleLogging == true {print("entered 2nd task.")}
  223. var requestBasal = URLRequest(url: urlBasal)
  224. requestBasal.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  225. let basalTask = URLSession.shared.dataTask(with: requestBasal) { data, response, error in
  226. if self.consoleLogging == true {print("in update loop.")}
  227. guard error == nil else {
  228. return
  229. }
  230. guard let data = data else {
  231. return
  232. }
  233. let json = try? JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]]
  234. if let json = json {
  235. DispatchQueue.main.async {
  236. self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
  237. }
  238. }
  239. else
  240. {
  241. return
  242. }
  243. if self.consoleLogging == true {print("finish pump update")}
  244. }
  245. basalTask.resume()
  246. }
  247. // Parse Device Status Data
  248. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  249. if consoleLogging == true {print("in updatePump")}
  250. if jsonDeviceStatus.count == 0 {
  251. return
  252. }
  253. //only grabbing one record since ns sorts by {created_at : -1}
  254. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  255. //pump and uploader
  256. let formatter = ISO8601DateFormatter()
  257. formatter.formatOptions = [.withFullDate,
  258. .withTime,
  259. .withDashSeparatorInDate,
  260. .withColonSeparatorInTime]
  261. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  262. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  263. if let reservoirData = lastPumpRecord["reservoir"] as? Double
  264. {
  265. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  266. } else {
  267. tableData[5].value = "50+U"
  268. }
  269. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  270. let upbat = uploader["battery"] as! Double
  271. // BatteryText.text! += " " + String(format:"%.0f", upbat) + "%"
  272. tableData[4].value = String(format:"%.0f", upbat) + "%"
  273. }
  274. }
  275. }
  276. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  277. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  278. if let failure = lastLoopRecord["failureReason"] {
  279. //LoopStatusText.text! += " Failure "
  280. }
  281. else
  282. {
  283. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  284. if let lastTempBasal = enacted["rate"] as? Double {
  285. tableData[2].value = String(format:"%.1f", lastTempBasal)
  286. }
  287. }
  288. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  289. tableData[0].value = String(format:"%.1f", (iobdata["iob"] as! Double))
  290. }
  291. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  292. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  293. }
  294. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  295. // let prediction = predictdata["values"] as! [Double]
  296. // loopStatusText += " EBG " + bgOutputFormat(bg: prediction.last!, mmol: mmol)
  297. }
  298. }
  299. }
  300. }
  301. var oText = "" as String
  302. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  303. if let lastOverrideTime = formatter.date(from: (lastOverride["timestamp"] as! String))?.timeIntervalSince1970 {
  304. }
  305. if lastOverride["active"] as! Bool {
  306. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  307. if let multiplier = lastOverride["multiplier"] as? Double {
  308. oText += String(format:"%.1f", multiplier*100)
  309. }
  310. else
  311. {
  312. oText += String(format:"%.1f", 100)
  313. }
  314. oText += "% ("
  315. let minValue = lastCorrection["minValue"] as! Double
  316. let maxValue = lastCorrection["maxValue"] as! Double
  317. oText += bgOutputFormat(bg: minValue, mmol: mmol) + "-" + bgOutputFormat(bg: maxValue, mmol: mmol) + ")"
  318. tableData[3].value = oText
  319. }
  320. }
  321. infoTable.reloadData()
  322. }
  323. func stringFromTimeInterval(interval: TimeInterval) -> String {
  324. let interval = Int(interval)
  325. let minutes = (interval / 60) % 60
  326. let hours = (interval / 3600)
  327. return String(format: "%02d:%02d", hours, minutes)
  328. }
  329. func createGraph(entries: [sgvData]){
  330. var bgChartEntry = [ChartDataEntry]()
  331. var colors = [NSUIColor]()
  332. for i in 0..<entries.count{
  333. var dateString = String(entries[entries.count - 1 - i].date).prefix(10)
  334. let dateSecondsOnly = Double(String(dateString))!
  335. let value = ChartDataEntry(x: Double(dateSecondsOnly), y: Double(entries[entries.count - 1 - i].sgv))
  336. bgChartEntry.append(value)
  337. if Double(entries[entries.count - 1 - i].sgv) > 180 {
  338. colors.append(NSUIColor.yellow)
  339. } else if Double(entries[entries.count - 1 - i].sgv) < 70 {
  340. colors.append(NSUIColor.red)
  341. } else {
  342. colors.append(NSUIColor.green)
  343. }
  344. }
  345. let line1 = LineChartDataSet(entries:bgChartEntry, label: "")
  346. line1.circleRadius = 3
  347. line1.circleColors = [NSUIColor.green]
  348. line1.drawCircleHoleEnabled = false
  349. if UserDefaultsRepository.showLines.value {
  350. line1.lineWidth = 2
  351. } else {
  352. line1.lineWidth = 0
  353. }
  354. if UserDefaultsRepository.showDots.value {
  355. line1.drawCirclesEnabled = true
  356. } else {
  357. line1.drawCirclesEnabled = false
  358. }
  359. line1.setDrawHighlightIndicators(false)
  360. line1.valueFont.withSize(50)
  361. for i in 1..<colors.count{
  362. line1.addColor(colors[i])
  363. line1.circleColors.append(colors[i])
  364. }
  365. let data = LineChartData()
  366. data.addDataSet(line1)
  367. data.setValueFont(UIFont(name: UIFont.systemFont(ofSize: 10).fontName, size: 10)!)
  368. data.setDrawValues(false)
  369. BGChart.xAxis.valueFormatter = ChartXValueFormatter()
  370. BGChart.xAxis.granularity = 1800
  371. BGChart.xAxis.labelTextColor = NSUIColor.white
  372. BGChart.rightAxis.labelTextColor = NSUIColor.white
  373. BGChart.leftAxis.enabled = false
  374. BGChart.legend.enabled = false
  375. BGChart.scaleYEnabled = false
  376. BGChart.data = data
  377. BGChart.setExtraOffsets(left: 10, top: 10, right: 10, bottom: 10)
  378. BGChart.setVisibleXRangeMinimum(10)
  379. if firstStart {
  380. BGChart.zoom(scaleX: 20, scaleY: 1, x: 1, y: 1)
  381. firstStart = false
  382. }
  383. BGChart.moveViewToX(BGChart.chartXMax)
  384. //24 Hour Small Graph
  385. let line2 = LineChartDataSet(entries:bgChartEntry, label: "Number")
  386. line2.drawCirclesEnabled = false
  387. line2.lineWidth = 1
  388. for i in 1..<colors.count{
  389. line2.addColor(colors[i])
  390. line2.circleColors.append(colors[i])
  391. }
  392. let data2 = LineChartData()
  393. data2.addDataSet(line2)
  394. BGChartFull.leftAxis.enabled = false
  395. BGChartFull.rightAxis.enabled = false
  396. BGChartFull.xAxis.enabled = false
  397. BGChartFull.legend.enabled = false
  398. BGChartFull.scaleYEnabled = false
  399. BGChartFull.scaleXEnabled = false
  400. BGChartFull.drawGridBackgroundEnabled = false
  401. BGChartFull.data = data2
  402. }
  403. func updateBadge(entries: [sgvData]) {
  404. UIApplication.shared.applicationIconBadgeNumber = 0
  405. if entries.count > 0 {
  406. let latestBG = entries[0].sgv
  407. UIApplication.shared.applicationIconBadgeNumber = latestBG
  408. }
  409. print("updated badge")
  410. }
  411. func updateBG (entries: [sgvData]) {
  412. if consoleLogging == true {print("in update BG")}
  413. UIApplication.shared.applicationIconBadgeNumber = 0
  414. if entries.count > 0 {
  415. let latestBG = entries[0].sgv
  416. let priorBG = entries[1].sgv
  417. let deltaBG = latestBG - priorBG as Int
  418. let lastBGTime = entries[0].date / 1000 //NS has different units
  419. let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-lastBGTime) / 60
  420. var userUnit = " mg/dL"
  421. if mmol {
  422. userUnit = " mmol/L"
  423. }
  424. UIApplication.shared.applicationIconBadgeNumber = latestBG
  425. BGText.text = bgOutputFormat(bg: Double(latestBG), mmol: mmol)
  426. MinAgoText.text = String(Int(deltaTime)) + " min ago"
  427. print(String(Int(deltaTime)) + " min ago")
  428. if let directionBG = entries[0].direction {
  429. DirectionText.text = bgDirectionGraphic(directionBG)
  430. }
  431. else
  432. {
  433. DirectionText.text = ""
  434. }
  435. if deltaBG < 0 {
  436. self.DeltaText.text = String(deltaBG)
  437. }
  438. else
  439. {
  440. self.DeltaText.text = "+" + String(deltaBG)
  441. }
  442. }
  443. else
  444. {
  445. return
  446. }
  447. if consoleLogging == true {print("end update bg")}
  448. }
  449. func bgOutputFormat(bg: Double, mmol: Bool) -> String {
  450. if !mmol {
  451. return String(format:"%.0f", bg)
  452. }
  453. else
  454. {
  455. return String(format:"%.1f", bg / 18.0)
  456. }
  457. }
  458. func bgDirectionGraphic(_ value:String)->String {
  459. 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":"-"]
  460. return graphics[value]!
  461. }
  462. }