NightScout.swift 40 KB

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