NightScout.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  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. chartDispatch.notify(queue: .main){
  43. self.updateBadge()
  44. self.viewUpdateNSBG()
  45. if UserDefaultsRepository.writeCalendarEvent.value {
  46. self.writeCalendar()
  47. }
  48. self.createGraph()
  49. }
  50. } else {
  51. // Dispatch and process json for all web calls before proceeeding
  52. webLoadNSProfile()
  53. WebLoadNSTempBasals()
  54. webLoadNSDeviceStatus()
  55. webLoadNSBoluses()
  56. webLoadNSCarbs()
  57. chartDispatch.notify(queue: .main){
  58. self.createGraph()
  59. self.updateMinAgo()
  60. self.clearOldSnoozes()
  61. self.checkAlarms(bgs: self.bgData)
  62. }
  63. }
  64. }
  65. // NS BG Data Web call
  66. func webLoadNSBGData(onlyPullLastRecord: Bool = false) {
  67. chartDispatch.enter()
  68. // Set the count= in the url either to pull 24 hours or only the last record
  69. var points = "1"
  70. if !onlyPullLastRecord {
  71. points = String(self.graphHours * 12 + 1)
  72. }
  73. // URL processor
  74. var urlBGDataPath: String = UserDefaultsRepository.url.value + "/api/v1/entries/sgv.json?"
  75. if token == "" {
  76. urlBGDataPath = urlBGDataPath + "count=" + points
  77. } else {
  78. urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
  79. }
  80. guard let urlBGData = URL(string: urlBGDataPath) else {
  81. return
  82. }
  83. var request = URLRequest(url: urlBGData)
  84. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  85. // Downloader
  86. let getBGTask = URLSession.shared.dataTask(with: request) { data, response, error in
  87. if self.consoleLogging == true {print("start bg url")}
  88. guard error == nil else {
  89. return
  90. }
  91. guard let data = data else {
  92. return
  93. }
  94. let decoder = JSONDecoder()
  95. let entriesResponse = try? decoder.decode([sgvData].self, from: data)
  96. if let entriesResponse = entriesResponse {
  97. DispatchQueue.main.async {
  98. // trigger the processor for the data after downloading.
  99. self.ProcessNSBGData(data: entriesResponse, onlyPullLastRecord: onlyPullLastRecord)
  100. }
  101. } else {
  102. return
  103. }
  104. }
  105. getBGTask.resume()
  106. }
  107. // NS BG Data Response processor
  108. func ProcessNSBGData(data: [sgvData], onlyPullLastRecord: Bool){
  109. defer { chartDispatch.leave() }
  110. var pullDate = data[data.count - 1].date / 1000
  111. pullDate.round(FloatingPointRoundingRule.toNearestOrEven)
  112. // 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
  113. if !onlyPullLastRecord {
  114. bgData.removeAll()
  115. } else if bgData[bgData.count - 1].date != pullDate {
  116. bgData.removeFirst()
  117. } else {
  118. // Update the badge, bg, graph settings even if we don't have a new reading.
  119. self.updateBadge()
  120. self.viewUpdateNSBG()
  121. return
  122. }
  123. // 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.
  124. for i in 0..<data.count{
  125. var dateString = data[data.count - 1 - i].date / 1000
  126. dateString.round(FloatingPointRoundingRule.toNearestOrEven)
  127. let reading = sgvData(sgv: data[data.count - 1 - i].sgv, date: dateString, direction: data[data.count - 1 - i].direction)
  128. bgData.append(reading)
  129. }
  130. }
  131. // NS BG Data Front end updater
  132. func viewUpdateNSBG () {
  133. let entries = bgData
  134. if entries.count > 0 {
  135. let latestEntryi = entries.count - 1
  136. let latestBG = entries[latestEntryi].sgv
  137. let priorBG = entries[latestEntryi - 1].sgv
  138. let deltaBG = latestBG - priorBG as Int
  139. let lastBGTime = entries[latestEntryi].date
  140. let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-lastBGTime) / 60
  141. var userUnit = " mg/dL"
  142. if mmol {
  143. userUnit = " mmol/L"
  144. }
  145. if UserDefaultsRepository.appBadge.value {
  146. UIApplication.shared.applicationIconBadgeNumber = latestBG
  147. } else {
  148. UIApplication.shared.applicationIconBadgeNumber = 0
  149. }
  150. BGText.text = bgOutputFormat(bg: Double(latestBG), mmol: mmol)
  151. setBGTextColor()
  152. MinAgoText.text = String(Int(deltaTime)) + " min ago"
  153. print(String(Int(deltaTime)) + " min ago")
  154. if let directionBG = entries[latestEntryi].direction {
  155. DirectionText.text = bgDirectionGraphic(directionBG)
  156. }
  157. else
  158. {
  159. DirectionText.text = ""
  160. }
  161. if deltaBG < 0 {
  162. self.DeltaText.text = String(deltaBG)
  163. }
  164. else
  165. {
  166. self.DeltaText.text = "+" + String(deltaBG)
  167. }
  168. }
  169. else
  170. {
  171. return
  172. }
  173. checkAlarms(bgs: entries)
  174. }
  175. // NS Device Status Web Call
  176. func webLoadNSDeviceStatus() {
  177. self.chartDispatch.enter()
  178. let urlUser = UserDefaultsRepository.url.value
  179. var urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=1"
  180. if token != "" {
  181. urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?token=" + token + "&count=1"
  182. }
  183. let escapedAddress = urlStringDeviceStatus.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  184. guard let urlDeviceStatus = URL(string: escapedAddress!) else {
  185. return
  186. }
  187. if consoleLogging == true {print("entered device status task.")}
  188. var requestDeviceStatus = URLRequest(url: urlDeviceStatus)
  189. requestDeviceStatus.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  190. let deviceStatusTask = URLSession.shared.dataTask(with: requestDeviceStatus) { data, response, error in
  191. if self.consoleLogging == true {print("in device status loop.")}
  192. guard error == nil else {
  193. return
  194. }
  195. guard let data = data else {
  196. return
  197. }
  198. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  199. if let json = json {
  200. DispatchQueue.main.async {
  201. self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
  202. }
  203. } else {
  204. return
  205. }
  206. if self.consoleLogging == true {print("finish pump update")}}
  207. deviceStatusTask.resume()
  208. }
  209. // NS Device Status Response Processor
  210. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  211. defer { chartDispatch.leave() }
  212. if consoleLogging == true {print("in updatePump")}
  213. if jsonDeviceStatus.count == 0 {
  214. return
  215. }
  216. //only grabbing one record since ns sorts by {created_at : -1}
  217. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  218. //pump and uploader
  219. let formatter = ISO8601DateFormatter()
  220. formatter.formatOptions = [.withFullDate,
  221. .withTime,
  222. .withDashSeparatorInDate,
  223. .withColonSeparatorInTime]
  224. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  225. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  226. if let reservoirData = lastPumpRecord["reservoir"] as? Double
  227. {
  228. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  229. } else {
  230. tableData[5].value = "50+U"
  231. }
  232. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  233. let upbat = uploader["battery"] as! Double
  234. tableData[4].value = String(format:"%.0f", upbat) + "%"
  235. }
  236. }
  237. }
  238. // Loop
  239. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  240. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  241. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  242. if let failure = lastLoopRecord["failureReason"] {
  243. LoopStatusLabel.text = "⚠"
  244. }
  245. else
  246. {
  247. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  248. if let lastTempBasal = enacted["rate"] as? Double {
  249. // tableData[2].value = String(format:"%.1f", lastTempBasal)
  250. }
  251. }
  252. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  253. tableData[0].value = String(format:"%.1f", (iobdata["iob"] as! Double))
  254. }
  255. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  256. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  257. }
  258. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  259. let prediction = predictdata["values"] as! [Double]
  260. PredictionLabel.text = String(Int(prediction.last!))
  261. PredictionLabel.textColor = UIColor.systemPurple
  262. predictionData.removeAll()
  263. var i = 1
  264. while i <= 12 {
  265. predictionData.append(prediction[i])
  266. i += 1
  267. }
  268. }
  269. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  270. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  271. if tempBasalTime > lastLoopTime {
  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. }
  285. }
  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. self.chartDispatch.enter()
  436. let urlString = UserDefaultsRepository.url.value + "/api/v1/profile/current.json"
  437. let escapedAddress = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  438. guard let url = URL(string: escapedAddress!) else {
  439. return
  440. }
  441. var request = URLRequest(url: url)
  442. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  443. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  444. guard error == nil else {
  445. return
  446. }
  447. guard let data = data else {
  448. return
  449. }
  450. let json = try? JSONSerialization.jsonObject(with: data) as! Dictionary<String, Any>
  451. if let json = json {
  452. DispatchQueue.main.async {
  453. self.updateProfile(jsonDeviceStatus: json)
  454. }
  455. } else {
  456. return
  457. }
  458. }
  459. task.resume()
  460. }
  461. // NS Profile Response Processor
  462. func updateProfile(jsonDeviceStatus: Dictionary<String, Any>) {
  463. defer { chartDispatch.leave() }
  464. if jsonDeviceStatus.count == 0 {
  465. return
  466. }
  467. let basal = jsonDeviceStatus[keyPath: "store.Default.basal"] as! NSArray
  468. for i in 0..<basal.count {
  469. let dict = basal[i] as! Dictionary<String, Any>
  470. let thisValue = dict[keyPath: "value"] as! Double
  471. let thisTime = dict[keyPath: "time"] as! String
  472. let thisTimeAsSeconds = dict[keyPath: "timeAsSeconds"] as! Double
  473. let entry = basalProfileStruct(value: thisValue, time: thisTime, timeAsSeconds: thisTimeAsSeconds)
  474. basalProfile.append(entry)
  475. }
  476. }
  477. // NS Temp Basal Web Call
  478. func WebLoadNSTempBasals() {
  479. self.chartDispatch.enter()
  480. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  481. var urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  482. if token != "" {
  483. urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  484. }
  485. guard let urlData = URL(string: urlString) else {
  486. return
  487. }
  488. var request = URLRequest(url: urlData)
  489. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  490. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  491. guard error == nil else {
  492. return
  493. }
  494. guard let data = data else {
  495. return
  496. }
  497. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  498. if let json = json {
  499. DispatchQueue.main.async {
  500. self.updateBasals(entries: json)
  501. }
  502. } else {
  503. return
  504. }
  505. }
  506. task.resume()
  507. }
  508. // NS Temp Basal Response Processor
  509. func updateBasals(entries: [[String:AnyObject]]) {
  510. defer { chartDispatch.leave() }
  511. // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
  512. basalData.removeAll()
  513. var lastEndDot = 0.0
  514. var tempArray: [basalGraphStruct] = []
  515. for i in 0..<entries.count {
  516. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  517. let basalDate = currentEntry?["timestamp"] as! String
  518. let strippedZone = String(basalDate.dropLast())
  519. let dateFormatter = DateFormatter()
  520. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  521. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  522. let dateString = dateFormatter.date(from: strippedZone)
  523. let dateTimeStamp = dateString!.timeIntervalSince1970
  524. let basalRate = currentEntry?["absolute"] as! Double
  525. let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
  526. // Setting end dots
  527. var duration = 0.0
  528. // 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
  529. if i <= entries.count - 1 {
  530. duration = currentEntry?["duration"] as! Double
  531. } else {
  532. duration = dateTimeStamp + 60
  533. }
  534. // 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
  535. if i > 0 {
  536. let priorEntry = entries[entries.count - i] as [String : AnyObject]?
  537. let priorBasalDate = priorEntry?["timestamp"] as! String
  538. let priorStrippedZone = String(priorBasalDate.dropLast())
  539. let priorDateFormatter = DateFormatter()
  540. priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  541. priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  542. let priorDateString = dateFormatter.date(from: priorStrippedZone)
  543. let priorDateTimeStamp = priorDateString!.timeIntervalSince1970
  544. let priorDuration = priorEntry?["duration"] as! Double
  545. // if difference between time stamps is greater than the duration of the last entry, there is a gap
  546. if Double( dateTimeStamp - priorDateTimeStamp ) > Double( priorDuration * 60 ) {
  547. var scheduled = 0.0
  548. // cycle through basal profiles.
  549. // TODO figure out how to deal with profile changes that happen mid-gap
  550. for b in 0..<self.basalProfile.count {
  551. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  552. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  553. // check the prior temp ending to the profile seconds from midnight
  554. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeYesterday {
  555. scheduled = basalProfile[b].value
  556. }
  557. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeToday {
  558. scheduled = basalProfile[b].value
  559. }
  560. // This will iterate through from midnight on and set it for the highest matching one.
  561. }
  562. // Make the starting dot at the last ending dot
  563. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
  564. basalData.append(startDot)
  565. // Make the ending dot at the new starting dot
  566. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
  567. basalData.append(endDot)
  568. }
  569. }
  570. // Make the starting dot
  571. let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))
  572. basalData.append(startDot)
  573. // Make the ending dot
  574. // If it's the last one and not ended yet, extend it for 1 hour to match the prediction length. Otherwise let it end
  575. if i == entries.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
  576. lastEndDot = Date().timeIntervalSince1970 + (55 * 60)
  577. tableData[2].value = String(format:"%.1f", basalRate)
  578. } else {
  579. lastEndDot = dateTimeStamp + (duration * 60)
  580. }
  581. let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))
  582. basalData.append(endDot)
  583. }
  584. // If last scheduled basal was prior to right now, we need to create one last scheduled entry
  585. if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() {
  586. var scheduled = 0.0
  587. // cycle through basal profiles.
  588. // TODO figure out how to deal with profile changes that happen mid-gap
  589. for b in 0..<self.basalProfile.count {
  590. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  591. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  592. // check the prior temp ending to the profile seconds from midnight
  593. if lastEndDot >= scheduleTimeYesterday {
  594. scheduled = basalProfile[b].value
  595. }
  596. if lastEndDot >= scheduleTimeToday {
  597. scheduled = basalProfile[b].value
  598. }
  599. }
  600. tableData[2].value = String(format:"%.1f", scheduled)
  601. // Make the starting dot at the last ending dot
  602. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
  603. basalData.append(startDot)
  604. // Make the ending dot 1 hour after now
  605. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970))
  606. basalData.append(endDot)
  607. }
  608. }
  609. // NS Bolus Web Call
  610. func webLoadNSBoluses(){
  611. self.chartDispatch.enter()
  612. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  613. let urlUser = UserDefaultsRepository.url.value
  614. var searchString = "find[eventType]=Correction%20Bolus&find[created_at][$gte]=" + yesterdayString
  615. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  616. if token == "" {
  617. urlDataPath = urlDataPath + searchString
  618. }
  619. else
  620. {
  621. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  622. }
  623. guard let urlData = URL(string: urlDataPath) else {
  624. return
  625. }
  626. var request = URLRequest(url: urlData)
  627. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  628. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  629. guard error == nil else {
  630. return
  631. }
  632. guard let data = data else {
  633. return
  634. }
  635. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  636. if let json = json {
  637. DispatchQueue.main.async {
  638. self.processNSBolus(entries: json)
  639. }
  640. } else {
  641. return
  642. }
  643. }
  644. task.resume()
  645. }
  646. // NS Meal Bolus Response Processor
  647. func processNSBolus(entries: [[String:AnyObject]]) {
  648. defer { chartDispatch.leave() }
  649. // because it's a small array, we're going to destroy and reload every time.
  650. bolusData.removeAll()
  651. var lastFoundIndex = 0
  652. for i in 0..<entries.count {
  653. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  654. let bolusDate = currentEntry?["timestamp"] as! String
  655. let strippedZone = String(bolusDate.dropLast())
  656. let dateFormatter = DateFormatter()
  657. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  658. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  659. let dateString = dateFormatter.date(from: strippedZone)
  660. let dateTimeStamp = dateString!.timeIntervalSince1970
  661. let bolus = currentEntry?["insulin"] as! Double
  662. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  663. lastFoundIndex = sgv.foundIndex
  664. // Make the dot
  665. let dot = bolusCarbGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  666. bolusData.append(dot)
  667. }
  668. }
  669. // NS Carb Web Call
  670. func webLoadNSCarbs(){
  671. self.chartDispatch.enter()
  672. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  673. let urlUser = UserDefaultsRepository.url.value
  674. var searchString = "find[eventType]=Meal%20Bolus&find[created_at][$gte]=" + yesterdayString
  675. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  676. if token == "" {
  677. urlDataPath = urlDataPath + searchString
  678. }
  679. else
  680. {
  681. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  682. }
  683. guard let urlData = URL(string: urlDataPath) else {
  684. return
  685. }
  686. var request = URLRequest(url: urlData)
  687. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  688. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  689. guard error == nil else {
  690. return
  691. }
  692. guard let data = data else {
  693. return
  694. }
  695. let json = try? (JSONSerialization.jsonObject(with: data) as! [[String:AnyObject]])
  696. if let json = json {
  697. DispatchQueue.main.async {
  698. self.processNSCarbs(entries: json)
  699. }
  700. } else {
  701. return
  702. }
  703. }
  704. task.resume()
  705. }
  706. // NS Carb Bolus Response Processor
  707. func processNSCarbs(entries: [[String:AnyObject]]) {
  708. defer { chartDispatch.leave() }
  709. // because it's a small array, we're going to destroy and reload every time.
  710. carbData.removeAll()
  711. var lastFoundIndex = 0
  712. for i in 0..<entries.count {
  713. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  714. let bolusDate = currentEntry?["timestamp"] as! String
  715. let strippedZone = String(bolusDate.dropLast())
  716. let dateFormatter = DateFormatter()
  717. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  718. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  719. let dateString = dateFormatter.date(from: strippedZone)
  720. let dateTimeStamp = dateString!.timeIntervalSince1970
  721. let carbs = currentEntry?["carbs"] as! Double
  722. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  723. lastFoundIndex = sgv.foundIndex
  724. // Make the dot
  725. let dot = bolusCarbGraphStruct(value: carbs, date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  726. carbData.append(dot)
  727. }
  728. }
  729. }