NightScout.swift 36 KB

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