NightScout.swift 38 KB

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