NightScout.swift 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. //
  2. // NightScout.swift
  3. // LoopFollow
  4. //
  5. // Created by Jon Fawcett on 6/16/20.
  6. // Copyright © 2020 Jon Fawcett. All rights reserved.
  7. //
  8. import Foundation
  9. import UIKit
  10. extension MainViewController {
  11. // Main loader for all data
  12. func nightscoutLoader(forceLoad: Bool = false) {
  13. var needsLoaded: Bool = false
  14. var onlyPullLastRecord = false
  15. // If we have existing data and it's within 5 minutes, we aren't going to do a BG network call
  16. if bgData.count > 0 {
  17. let now = NSDate().timeIntervalSince1970
  18. let lastReadingTime = bgData[bgData.count - 1].date
  19. let secondsAgo = now - lastReadingTime
  20. if secondsAgo >= 5*60 {
  21. needsLoaded = true
  22. if secondsAgo < 10*60 {
  23. onlyPullLastRecord = true
  24. }
  25. }
  26. } else {
  27. needsLoaded = true
  28. }
  29. if forceLoad { needsLoaded = true}
  30. // Only do the network calls if we don't have a current reading
  31. if needsLoaded {
  32. self.clearLastInfoData()
  33. // Dispatch and process json for all web calls before proceeeding
  34. webLoadNSProfile()
  35. WebLoadNSTempBasals()
  36. webLoadNSDeviceStatus()
  37. webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  38. webLoadNSBoluses()
  39. webLoadNSCarbs()
  40. webLoadNSCage()
  41. webLoadNSSage()
  42. self.startViewTimer(time: viewTimeInterval)
  43. } else {
  44. // Dispatch and process json for all web calls before proceeeding
  45. webLoadNSProfile()
  46. WebLoadNSTempBasals()
  47. webLoadNSDeviceStatus()
  48. webLoadNSBoluses()
  49. webLoadNSCarbs()
  50. if bgData.count > 0 {
  51. self.viewUpdateNSBG()
  52. self.createGraph()
  53. self.updateMinAgo()
  54. self.clearOldSnoozes()
  55. self.checkAlarms(bgs: self.bgData)
  56. }
  57. }
  58. }
  59. // NS BG Data Web call
  60. func webLoadNSBGData(onlyPullLastRecord: Bool = false) {
  61. // Set the count= in the url either to pull 24 hours or only the last record
  62. var points = "1"
  63. if !onlyPullLastRecord {
  64. points = String(self.graphHours * 12 + 1)
  65. }
  66. // URL processor
  67. var urlBGDataPath: String = UserDefaultsRepository.url.value + "/api/v1/entries/sgv.json?"
  68. if token == "" {
  69. urlBGDataPath = urlBGDataPath + "count=" + points
  70. } else {
  71. urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
  72. }
  73. guard let urlBGData = URL(string: urlBGDataPath) else {
  74. return
  75. }
  76. var request = URLRequest(url: urlBGData)
  77. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  78. // Downloader
  79. let getBGTask = URLSession.shared.dataTask(with: request) { data, response, error in
  80. if self.consoleLogging == true {print("start bg url")}
  81. guard error == nil else {
  82. return
  83. }
  84. guard let data = data else {
  85. return
  86. }
  87. let decoder = JSONDecoder()
  88. let entriesResponse = try? decoder.decode([sgvData].self, from: data)
  89. if let entriesResponse = entriesResponse {
  90. DispatchQueue.main.async {
  91. // trigger the processor for the data after downloading.
  92. self.ProcessNSBGData(data: entriesResponse, onlyPullLastRecord: onlyPullLastRecord)
  93. }
  94. } else {
  95. return
  96. }
  97. }
  98. getBGTask.resume()
  99. }
  100. // NS BG Data Response processor
  101. func ProcessNSBGData(data: [sgvData], onlyPullLastRecord: Bool){
  102. var pullDate = data[data.count - 1].date / 1000
  103. pullDate.round(FloatingPointRoundingRule.toNearestOrEven)
  104. // If we already have data, we're going to pop it to the end and remove the first. If we have old or no data, we'll destroy the whole array and start over. This is simpler than determining how far back we need to get new data from in case Dex back-filled readings
  105. if !onlyPullLastRecord {
  106. bgData.removeAll()
  107. } else if bgData[bgData.count - 1].date != pullDate {
  108. bgData.removeFirst()
  109. if data.count > 0 && UserDefaultsRepository.speakBG.value {
  110. speakBG(sgv: data[data.count - 1].sgv)
  111. }
  112. } else {
  113. // Update the badge, bg, graph settings even if we don't have a new reading.
  114. self.updateBadge()
  115. self.viewUpdateNSBG()
  116. return
  117. }
  118. // loop through the data so we can reverse the order to oldest first for the graph and convert the NS timestamp to seconds instead of milliseconds. Makes date comparisons easier for everything else.
  119. for i in 0..<data.count{
  120. var dateString = data[data.count - 1 - i].date / 1000
  121. dateString.round(FloatingPointRoundingRule.toNearestOrEven)
  122. let reading = sgvData(sgv: data[data.count - 1 - i].sgv, date: dateString, direction: data[data.count - 1 - i].direction)
  123. bgData.append(reading)
  124. }
  125. if firstGraphLoad {
  126. viewUpdateNSBG()
  127. createGraph()
  128. }
  129. }
  130. // NS BG Data Front end updater
  131. func viewUpdateNSBG () {
  132. let entries = bgData
  133. if entries.count > 0 {
  134. let latestEntryi = entries.count - 1
  135. let latestBG = entries[latestEntryi].sgv
  136. let priorBG = entries[latestEntryi - 1].sgv
  137. let deltaBG = latestBG - priorBG as Int
  138. let lastBGTime = entries[latestEntryi].date
  139. let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-lastBGTime) / 60
  140. var userUnit = " mg/dL"
  141. if mmol {
  142. userUnit = " mmol/L"
  143. }
  144. if UserDefaultsRepository.appBadge.value {
  145. UIApplication.shared.applicationIconBadgeNumber = latestBG
  146. } else {
  147. UIApplication.shared.applicationIconBadgeNumber = 0
  148. }
  149. BGText.text = bgOutputFormat(bg: Double(latestBG), mmol: mmol)
  150. setBGTextColor()
  151. MinAgoText.text = String(Int(deltaTime)) + " min ago"
  152. print(String(Int(deltaTime)) + " min ago")
  153. if let directionBG = entries[latestEntryi].direction {
  154. DirectionText.text = bgDirectionGraphic(directionBG)
  155. }
  156. else
  157. {
  158. DirectionText.text = ""
  159. }
  160. if deltaBG < 0 {
  161. self.DeltaText.text = String(deltaBG)
  162. }
  163. else
  164. {
  165. self.DeltaText.text = "+" + String(deltaBG)
  166. }
  167. }
  168. else
  169. {
  170. return
  171. }
  172. checkAlarms(bgs: entries)
  173. }
  174. // NS Device Status Web Call
  175. func webLoadNSDeviceStatus() {
  176. let urlUser = UserDefaultsRepository.url.value
  177. var urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=1"
  178. if token != "" {
  179. urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?token=" + token + "&count=1"
  180. }
  181. let escapedAddress = urlStringDeviceStatus.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  182. guard let urlDeviceStatus = URL(string: escapedAddress!) else {
  183. return
  184. }
  185. if consoleLogging == true {print("entered device status task.")}
  186. var requestDeviceStatus = URLRequest(url: urlDeviceStatus)
  187. requestDeviceStatus.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  188. let deviceStatusTask = URLSession.shared.dataTask(with: requestDeviceStatus) { data, response, error in
  189. if self.consoleLogging == true {print("in device status loop.")}
  190. guard error == nil else {
  191. return
  192. }
  193. guard let data = data else {
  194. return
  195. }
  196. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  197. if let json = json {
  198. DispatchQueue.main.async {
  199. self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
  200. }
  201. } else {
  202. return
  203. }
  204. if self.consoleLogging == true {print("finish pump update")}}
  205. deviceStatusTask.resume()
  206. }
  207. // NS Device Status Response Processor
  208. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  209. if consoleLogging == true {print("in updatePump")}
  210. if jsonDeviceStatus.count == 0 {
  211. return
  212. }
  213. //only grabbing one record since ns sorts by {created_at : -1}
  214. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  215. //pump and uploader
  216. let formatter = ISO8601DateFormatter()
  217. formatter.formatOptions = [.withFullDate,
  218. .withTime,
  219. .withDashSeparatorInDate,
  220. .withColonSeparatorInTime]
  221. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  222. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  223. if let reservoirData = lastPumpRecord["reservoir"] as? Double {
  224. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  225. } else {
  226. tableData[5].value = "50+U"
  227. }
  228. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  229. let upbat = uploader["battery"] as! Double
  230. tableData[4].value = String(format:"%.0f", upbat) + "%"
  231. }
  232. }
  233. }
  234. // Loop
  235. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  236. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  237. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  238. if let failure = lastLoopRecord["failureReason"] {
  239. LoopStatusLabel.text = "X"
  240. } else {
  241. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  242. if let lastTempBasal = enacted["rate"] as? Double {
  243. // tableData[2].value = String(format:"%.1f", lastTempBasal)
  244. }
  245. }
  246. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  247. tableData[0].value = String(format:"%.1f", (iobdata["iob"] as! Double))
  248. }
  249. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  250. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  251. }
  252. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  253. let prediction = predictdata["values"] as! [Double]
  254. PredictionLabel.text = String(Int(prediction.last!))
  255. PredictionLabel.textColor = UIColor.systemPurple
  256. predictionData.removeAll()
  257. if UserDefaultsRepository.downloadPrediction.value {
  258. var i = 1
  259. while i <= 12 {
  260. predictionData.append(prediction[i])
  261. i += 1
  262. }
  263. }
  264. }
  265. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  266. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  267. var lastBGTime = lastLoopTime
  268. if bgData.count > 0 {
  269. lastBGTime = bgData[bgData.count - 1].date
  270. }
  271. if tempBasalTime > lastBGTime {
  272. LoopStatusLabel.text = "⏀"
  273. } else {
  274. LoopStatusLabel.text = "↻"
  275. }
  276. }
  277. } else {
  278. LoopStatusLabel.text = "↻"
  279. }
  280. }
  281. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 10 {
  282. LoopStatusLabel.text = "⚠"
  283. }
  284. } // end lastLoopTime
  285. } // end lastLoop Record
  286. var oText = "" as String
  287. currentOverride = 1.0
  288. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  289. if let lastOverrideTime = formatter.date(from: (lastOverride["timestamp"] as! String))?.timeIntervalSince1970 {
  290. }
  291. if lastOverride["active"] as! Bool {
  292. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  293. if let multiplier = lastOverride["multiplier"] as? Double {
  294. currentOverride = multiplier
  295. oText += String(format:"%.1f", multiplier*100)
  296. }
  297. else
  298. {
  299. oText += String(format:"%.1f", 100)
  300. }
  301. oText += "% ("
  302. let minValue = lastCorrection["minValue"] as! Double
  303. let maxValue = lastCorrection["maxValue"] as! Double
  304. oText += bgOutputFormat(bg: minValue, mmol: mmol) + "-" + bgOutputFormat(bg: maxValue, mmol: mmol) + ")"
  305. tableData[3].value = oText
  306. }
  307. }
  308. infoTable.reloadData()
  309. }
  310. // NS Cage Web Call
  311. func webLoadNSCage() {
  312. let urlUser = UserDefaultsRepository.url.value
  313. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Site%20Change&count=1"
  314. if token != "" {
  315. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Site%20Change&count=1"
  316. }
  317. guard let urlData = URL(string: urlString) else {
  318. return
  319. }
  320. var request = URLRequest(url: urlData)
  321. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  322. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  323. if self.consoleLogging == true {print("start cage url")}
  324. guard error == nil else {
  325. return
  326. }
  327. guard let data = data else {
  328. return
  329. }
  330. let decoder = JSONDecoder()
  331. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  332. if let entriesResponse = entriesResponse {
  333. DispatchQueue.main.async {
  334. self.updateCage(data: entriesResponse)
  335. }
  336. } else {
  337. return
  338. }
  339. }
  340. task.resume()
  341. }
  342. // NS Cage Response Processor
  343. func updateCage(data: [cageData]) {
  344. if consoleLogging == true {print("in updateCage")}
  345. if data.count == 0 {
  346. return
  347. }
  348. let lastCageString = data[0].created_at
  349. let formatter = ISO8601DateFormatter()
  350. formatter.formatOptions = [.withFullDate,
  351. .withTime,
  352. .withDashSeparatorInDate,
  353. .withColonSeparatorInTime]
  354. UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval
  355. if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 {
  356. let now = NSDate().timeIntervalSince1970
  357. let secondsAgo = now - cageTime
  358. //let days = 24 * 60 * 60
  359. let formatter = DateComponentsFormatter()
  360. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  361. formatter.allowedUnits = [ .hour, .minute ] // Units to display in the formatted string
  362. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  363. let formattedDuration = formatter.string(from: secondsAgo)
  364. tableData[7].value = formattedDuration ?? ""
  365. }
  366. infoTable.reloadData()
  367. }
  368. // NS Sage Web Call
  369. func webLoadNSSage() {
  370. var dayComponent = DateComponents()
  371. dayComponent.day = -10 // For removing 10 days
  372. let theCalendar = Calendar.current
  373. let startDate = theCalendar.date(byAdding: dayComponent, to: Date())!
  374. let dateFormatter = DateFormatter()
  375. dateFormatter.dateFormat = "yyyy-MM-dd"
  376. var startDateString = dateFormatter.string(from: startDate)
  377. let urlUser = UserDefaultsRepository.url.value
  378. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Sensor%20Start&find[created_at][$gte]=2020-05-31&count=1"
  379. if token != "" {
  380. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Sensor%20Start&find[created_at][$gte]=2020-05-31&count=1"
  381. }
  382. guard let urlData = URL(string: urlString) else {
  383. return
  384. }
  385. var request = URLRequest(url: urlData)
  386. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  387. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  388. if self.consoleLogging == true {print("start cage url")}
  389. guard error == nil else {
  390. return
  391. }
  392. guard let data = data else {
  393. return
  394. }
  395. let decoder = JSONDecoder()
  396. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  397. if let entriesResponse = entriesResponse {
  398. DispatchQueue.main.async {
  399. self.updateSage(data: entriesResponse)
  400. }
  401. } else {
  402. return
  403. }
  404. }
  405. task.resume()
  406. }
  407. // NS Sage Response Processor
  408. func updateSage(data: [cageData]) {
  409. if consoleLogging == true {print("in updateSage")}
  410. if data.count == 0 {
  411. return
  412. }
  413. var lastSageString = data[0].created_at
  414. let formatter = ISO8601DateFormatter()
  415. formatter.formatOptions = [.withFullDate,
  416. .withTime,
  417. .withDashSeparatorInDate,
  418. .withColonSeparatorInTime]
  419. UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: (lastSageString))?.timeIntervalSince1970 as! TimeInterval
  420. if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 {
  421. let now = NSDate().timeIntervalSince1970
  422. let secondsAgo = now - sageTime
  423. let days = 24 * 60 * 60
  424. let formatter = DateComponentsFormatter()
  425. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  426. formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
  427. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  428. let formattedDuration = formatter.string(from: secondsAgo)
  429. tableData[6].value = formattedDuration ?? ""
  430. }
  431. infoTable.reloadData()
  432. }
  433. // NS Profile Web Call
  434. func webLoadNSProfile() {
  435. let urlString = UserDefaultsRepository.url.value + "/api/v1/profile/current.json"
  436. let escapedAddress = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  437. guard let url = URL(string: escapedAddress!) else {
  438. return
  439. }
  440. var request = URLRequest(url: url)
  441. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  442. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  443. guard error == nil else {
  444. return
  445. }
  446. guard let data = data else {
  447. return
  448. }
  449. let json = try? JSONSerialization.jsonObject(with: data) as! Dictionary<String, Any>
  450. if let json = json {
  451. DispatchQueue.main.async {
  452. self.updateProfile(jsonDeviceStatus: json)
  453. }
  454. } else {
  455. return
  456. }
  457. }
  458. task.resume()
  459. }
  460. // NS Profile Response Processor
  461. func updateProfile(jsonDeviceStatus: Dictionary<String, Any>) {
  462. if jsonDeviceStatus.count == 0 {
  463. return
  464. }
  465. let basal = jsonDeviceStatus[keyPath: "store.Default.basal"] as! NSArray
  466. for i in 0..<basal.count {
  467. let dict = basal[i] as! Dictionary<String, Any>
  468. let thisValue = dict[keyPath: "value"] as! Double
  469. let thisTime = dict[keyPath: "time"] as! String
  470. let thisTimeAsSeconds = dict[keyPath: "timeAsSeconds"] as! Double
  471. let entry = basalProfileStruct(value: thisValue, time: thisTime, timeAsSeconds: thisTimeAsSeconds)
  472. basalProfile.append(entry)
  473. }
  474. }
  475. // NS Temp Basal Web Call
  476. func WebLoadNSTempBasals() {
  477. if !UserDefaultsRepository.downloadBasal.value { return }
  478. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  479. var urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  480. if token != "" {
  481. urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  482. }
  483. guard let urlData = URL(string: urlString) else {
  484. return
  485. }
  486. var request = URLRequest(url: urlData)
  487. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  488. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  489. guard error == nil else {
  490. return
  491. }
  492. guard let data = data else {
  493. return
  494. }
  495. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  496. if let json = json {
  497. DispatchQueue.main.async {
  498. self.updateBasals(entries: json)
  499. }
  500. } else {
  501. return
  502. }
  503. }
  504. task.resume()
  505. }
  506. // NS Temp Basal Response Processor
  507. func updateBasals(entries: [[String:AnyObject]]) {
  508. // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
  509. basalData.removeAll()
  510. var lastEndDot = 0.0
  511. var tempArray: [basalGraphStruct] = []
  512. for i in 0..<entries.count {
  513. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  514. let basalDate = currentEntry?["timestamp"] as! String
  515. let strippedZone = String(basalDate.dropLast())
  516. let dateFormatter = DateFormatter()
  517. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  518. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  519. let dateString = dateFormatter.date(from: strippedZone)
  520. let dateTimeStamp = dateString!.timeIntervalSince1970
  521. let basalRate = currentEntry?["absolute"] as! Double
  522. let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
  523. // Setting end dots
  524. var duration = 0.0
  525. // For all except the last we're going to use stored duration for end dot. For last we'll just put it 5 mintues after the entry
  526. if i <= entries.count - 1 {
  527. duration = currentEntry?["duration"] as! Double
  528. } else {
  529. duration = dateTimeStamp + 60
  530. }
  531. // This adds scheduled basal wherever there is a break between temps. can't check the prior ending on the first item. it is 24 hours old, so it isn't important for display anyway
  532. if i > 0 {
  533. let priorEntry = entries[entries.count - i] as [String : AnyObject]?
  534. let priorBasalDate = priorEntry?["timestamp"] as! String
  535. let priorStrippedZone = String(priorBasalDate.dropLast())
  536. let priorDateFormatter = DateFormatter()
  537. priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  538. priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  539. let priorDateString = dateFormatter.date(from: priorStrippedZone)
  540. let priorDateTimeStamp = priorDateString!.timeIntervalSince1970
  541. let priorDuration = priorEntry?["duration"] as! Double
  542. // if difference between time stamps is greater than the duration of the last entry, there is a gap
  543. if Double( dateTimeStamp - priorDateTimeStamp ) > Double( priorDuration * 60 ) {
  544. var scheduled = 0.0
  545. // cycle through basal profiles.
  546. // TODO figure out how to deal with profile changes that happen mid-gap
  547. for b in 0..<self.basalProfile.count {
  548. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  549. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  550. // check the prior temp ending to the profile seconds from midnight
  551. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeYesterday {
  552. scheduled = basalProfile[b].value
  553. }
  554. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeToday {
  555. scheduled = basalProfile[b].value
  556. }
  557. // This will iterate through from midnight on and set it for the highest matching one.
  558. }
  559. // Make the starting dot at the last ending dot
  560. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
  561. basalData.append(startDot)
  562. // Make the ending dot at the new starting dot
  563. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
  564. basalData.append(endDot)
  565. }
  566. }
  567. // Make the starting dot
  568. let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))
  569. basalData.append(startDot)
  570. // Make the ending dot
  571. // If it's the last one and not ended yet, extend it for 1 hour to match the prediction length. Otherwise let it end
  572. if i == entries.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
  573. lastEndDot = Date().timeIntervalSince1970 + (55 * 60)
  574. tableData[2].value = String(format:"%.1f", basalRate)
  575. } else {
  576. lastEndDot = dateTimeStamp + (duration * 60)
  577. }
  578. // Double check for overlaps of incorrect ended TBRs and sent it to end when the next one starts if it finds a discrepancy
  579. if i < entries.count - 1 {
  580. let nextEntry = entries[entries.count - 2 - i] as [String : AnyObject]?
  581. let nextBasalDate = nextEntry?["timestamp"] as! String
  582. let nextStrippedZone = String(nextBasalDate.dropLast())
  583. let nextDateFormatter = DateFormatter()
  584. nextDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  585. nextDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  586. let nextDateString = dateFormatter.date(from: nextStrippedZone)
  587. let nextDateTimeStamp = nextDateString!.timeIntervalSince1970
  588. if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) {
  589. lastEndDot = nextDateTimeStamp
  590. }
  591. }
  592. let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))
  593. basalData.append(endDot)
  594. }
  595. // If last scheduled basal was prior to right now, we need to create one last scheduled entry
  596. if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() {
  597. var scheduled = 0.0
  598. // cycle through basal profiles.
  599. // TODO figure out how to deal with profile changes that happen mid-gap
  600. for b in 0..<self.basalProfile.count {
  601. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  602. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  603. // check the prior temp ending to the profile seconds from midnight
  604. if lastEndDot >= scheduleTimeYesterday {
  605. scheduled = basalProfile[b].value
  606. }
  607. if lastEndDot >= scheduleTimeToday {
  608. scheduled = basalProfile[b].value
  609. }
  610. }
  611. tableData[2].value = String(format:"%.1f", scheduled)
  612. // Make the starting dot at the last ending dot
  613. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
  614. basalData.append(startDot)
  615. // Make the ending dot 1 hour after now
  616. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970))
  617. basalData.append(endDot)
  618. }
  619. }
  620. // NS Bolus Web Call
  621. func webLoadNSBoluses(){
  622. if !UserDefaultsRepository.downloadBolus.value { return }
  623. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  624. let urlUser = UserDefaultsRepository.url.value
  625. var searchString = "find[eventType]=Correction%20Bolus&find[created_at][$gte]=" + yesterdayString
  626. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  627. if token == "" {
  628. urlDataPath = urlDataPath + searchString
  629. }
  630. else
  631. {
  632. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  633. }
  634. guard let urlData = URL(string: urlDataPath) else {
  635. return
  636. }
  637. var request = URLRequest(url: urlData)
  638. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  639. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  640. guard error == nil else {
  641. return
  642. }
  643. guard let data = data else {
  644. return
  645. }
  646. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  647. if let json = json {
  648. DispatchQueue.main.async {
  649. self.processNSBolus(entries: json)
  650. }
  651. } else {
  652. return
  653. }
  654. }
  655. task.resume()
  656. }
  657. // NS Meal Bolus Response Processor
  658. func processNSBolus(entries: [[String:AnyObject]]) {
  659. // because it's a small array, we're going to destroy and reload every time.
  660. bolusData.removeAll()
  661. var lastFoundIndex = 0
  662. for i in 0..<entries.count {
  663. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  664. let bolusDate = currentEntry?["timestamp"] as! String
  665. let strippedZone = String(bolusDate.dropLast())
  666. let dateFormatter = DateFormatter()
  667. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  668. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  669. let dateString = dateFormatter.date(from: strippedZone)
  670. let dateTimeStamp = dateString!.timeIntervalSince1970
  671. let bolus = currentEntry?["insulin"] as! Double
  672. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  673. lastFoundIndex = sgv.foundIndex
  674. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  675. // Make the dot
  676. let dot = bolusCarbGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  677. bolusData.append(dot)
  678. }
  679. }
  680. }
  681. // NS Carb Web Call
  682. func webLoadNSCarbs(){
  683. if !UserDefaultsRepository.downloadCarbs.value { return }
  684. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  685. let urlUser = UserDefaultsRepository.url.value
  686. var searchString = "find[eventType]=Meal%20Bolus&find[created_at][$gte]=" + yesterdayString
  687. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  688. if token == "" {
  689. urlDataPath = urlDataPath + searchString
  690. }
  691. else
  692. {
  693. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  694. }
  695. guard let urlData = URL(string: urlDataPath) else {
  696. return
  697. }
  698. var request = URLRequest(url: urlData)
  699. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  700. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  701. guard error == nil else {
  702. return
  703. }
  704. guard let data = data else {
  705. return
  706. }
  707. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  708. if let json = json {
  709. DispatchQueue.main.async {
  710. self.processNSCarbs(entries: json)
  711. }
  712. } else {
  713. return
  714. }
  715. }
  716. task.resume()
  717. }
  718. // NS Carb Bolus Response Processor
  719. func processNSCarbs(entries: [[String:AnyObject]]) {
  720. // because it's a small array, we're going to destroy and reload every time.
  721. carbData.removeAll()
  722. var lastFoundIndex = 0
  723. for i in 0..<entries.count {
  724. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  725. let bolusDate = currentEntry?["timestamp"] as! String
  726. let strippedZone = String(bolusDate.dropLast())
  727. let dateFormatter = DateFormatter()
  728. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  729. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  730. let dateString = dateFormatter.date(from: strippedZone)
  731. let dateTimeStamp = dateString!.timeIntervalSince1970
  732. let carbs = currentEntry?["carbs"] as! Double
  733. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  734. lastFoundIndex = sgv.foundIndex
  735. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  736. // Make the dot
  737. let dot = bolusCarbGraphStruct(value: carbs, date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  738. carbData.append(dot)
  739. }
  740. }
  741. }
  742. }