NightScout.swift 38 KB

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