NightScout.swift 43 KB

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