NightScout.swift 37 KB

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