NightScout.swift 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076
  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. latestLoopTime = lastLoopTime
  286. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  287. if let failure = lastLoopRecord["failureReason"] {
  288. LoopStatusLabel.text = "X"
  289. latestLoopStatusString = "X"
  290. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  291. } else {
  292. var wasEnacted = false
  293. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  294. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  295. wasEnacted = true
  296. if let lastTempBasal = enacted["rate"] as? Double {
  297. // tableData[2].value = String(format:"%.1f", lastTempBasal)
  298. }
  299. }
  300. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  301. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  302. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  303. }
  304. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  305. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  306. latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
  307. }
  308. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  309. let prediction = predictdata["values"] as! [Double]
  310. PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
  311. PredictionLabel.textColor = UIColor.systemPurple
  312. predictionData.removeAll()
  313. if UserDefaultsRepository.downloadPrediction.value {
  314. var i = 1
  315. while i <= 12 {
  316. predictionData.append(prediction[i])
  317. i += 1
  318. }
  319. }
  320. }
  321. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  322. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  323. var lastBGTime = lastLoopTime
  324. if bgData.count > 0 {
  325. lastBGTime = bgData[bgData.count - 1].date
  326. }
  327. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  328. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  329. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  330. if tempBasalTime > lastBGTime && !wasEnacted {
  331. LoopStatusLabel.text = "⏀"
  332. latestLoopStatusString = "⏀"
  333. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  334. } else {
  335. LoopStatusLabel.text = "↻"
  336. latestLoopStatusString = "↻"
  337. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  338. }
  339. }
  340. } else {
  341. LoopStatusLabel.text = "↻"
  342. latestLoopStatusString = "↻"
  343. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  344. }
  345. }
  346. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 {
  347. LoopStatusLabel.text = "⚠"
  348. latestLoopStatusString = "⚠"
  349. }
  350. } // end lastLoopTime
  351. } // end lastLoop Record
  352. var oText = "" as String
  353. currentOverride = 1.0
  354. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  355. if let lastOverrideTime = formatter.date(from: (lastOverride["timestamp"] as! String))?.timeIntervalSince1970 {
  356. }
  357. if lastOverride["active"] as! Bool {
  358. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  359. if let multiplier = lastOverride["multiplier"] as? Double {
  360. currentOverride = multiplier
  361. oText += String(format:"%.1f", multiplier*100)
  362. }
  363. else
  364. {
  365. oText += String(format:"%.1f", 100)
  366. }
  367. oText += "% ("
  368. let minValue = lastCorrection["minValue"] as! Double
  369. let maxValue = lastCorrection["maxValue"] as! Double
  370. oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
  371. tableData[3].value = oText
  372. }
  373. }
  374. infoTable.reloadData()
  375. }
  376. // NS Cage Web Call
  377. func webLoadNSCage() {
  378. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: CAGE") }
  379. let urlUser = UserDefaultsRepository.url.value
  380. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Site%20Change&count=1"
  381. if token != "" {
  382. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Site%20Change&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. guard error == nil else {
  391. return
  392. }
  393. guard let data = data else {
  394. return
  395. }
  396. let decoder = JSONDecoder()
  397. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  398. if let entriesResponse = entriesResponse {
  399. DispatchQueue.main.async {
  400. self.updateCage(data: entriesResponse)
  401. }
  402. } else {
  403. return
  404. }
  405. }
  406. task.resume()
  407. }
  408. // NS Cage Response Processor
  409. func updateCage(data: [cageData]) {
  410. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: CAGE") }
  411. if data.count == 0 {
  412. return
  413. }
  414. let lastCageString = data[0].created_at
  415. let formatter = ISO8601DateFormatter()
  416. formatter.formatOptions = [.withFullDate,
  417. .withTime,
  418. .withDashSeparatorInDate,
  419. .withColonSeparatorInTime]
  420. UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval
  421. if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 {
  422. let now = NSDate().timeIntervalSince1970
  423. let secondsAgo = now - cageTime
  424. //let days = 24 * 60 * 60
  425. let formatter = DateComponentsFormatter()
  426. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  427. formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string
  428. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  429. let formattedDuration = formatter.string(from: secondsAgo)
  430. tableData[7].value = formattedDuration ?? ""
  431. }
  432. infoTable.reloadData()
  433. }
  434. // NS Sage Web Call
  435. func webLoadNSSage() {
  436. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: SAGE") }
  437. let lastDateString = dateTimeUtils.nowMinus10DaysTimeInterval()
  438. let urlUser = UserDefaultsRepository.url.value
  439. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  440. if token != "" {
  441. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  442. }
  443. guard let urlData = URL(string: urlString) else {
  444. return
  445. }
  446. var request = URLRequest(url: urlData)
  447. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  448. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  449. guard error == nil else {
  450. return
  451. }
  452. guard let data = data else {
  453. return
  454. }
  455. let decoder = JSONDecoder()
  456. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  457. if let entriesResponse = entriesResponse {
  458. DispatchQueue.main.async {
  459. self.updateSage(data: entriesResponse)
  460. }
  461. } else {
  462. return
  463. }
  464. }
  465. task.resume()
  466. }
  467. // NS Sage Response Processor
  468. func updateSage(data: [cageData]) {
  469. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process/Display: SAGE") }
  470. if data.count == 0 {
  471. return
  472. }
  473. var lastSageString = data[0].created_at
  474. let formatter = ISO8601DateFormatter()
  475. formatter.formatOptions = [.withFullDate,
  476. .withTime,
  477. .withDashSeparatorInDate,
  478. .withColonSeparatorInTime]
  479. UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: (lastSageString))?.timeIntervalSince1970 as! TimeInterval
  480. if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 {
  481. let now = NSDate().timeIntervalSince1970
  482. let secondsAgo = now - sageTime
  483. let days = 24 * 60 * 60
  484. let formatter = DateComponentsFormatter()
  485. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  486. formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
  487. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  488. let formattedDuration = formatter.string(from: secondsAgo)
  489. tableData[6].value = formattedDuration ?? ""
  490. }
  491. infoTable.reloadData()
  492. }
  493. // NS Profile Web Call
  494. func webLoadNSProfile() {
  495. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: profile") }
  496. let urlUser = UserDefaultsRepository.url.value
  497. var urlString = urlUser + "/api/v1/profile/current.json"
  498. if token != "" {
  499. urlString = urlUser + "/api/v1/profile/current.json?token=" + token
  500. }
  501. let escapedAddress = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  502. guard let url = URL(string: escapedAddress!) else {
  503. return
  504. }
  505. var request = URLRequest(url: url)
  506. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  507. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  508. guard error == nil else {
  509. return
  510. }
  511. guard let data = data else {
  512. return
  513. }
  514. let json = try? JSONSerialization.jsonObject(with: data) as! Dictionary<String, Any>
  515. if let json = json {
  516. DispatchQueue.main.async {
  517. self.updateProfile(jsonDeviceStatus: json)
  518. }
  519. } else {
  520. return
  521. }
  522. }
  523. task.resume()
  524. }
  525. // NS Profile Response Processor
  526. func updateProfile(jsonDeviceStatus: Dictionary<String, Any>) {
  527. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: profile") }
  528. if jsonDeviceStatus.count == 0 {
  529. return
  530. }
  531. if jsonDeviceStatus[keyPath: "message"] != nil { return }
  532. let basal = try jsonDeviceStatus[keyPath: "store.Default.basal"] as! NSArray
  533. for i in 0..<basal.count {
  534. let dict = basal[i] as! Dictionary<String, Any>
  535. do {
  536. let thisValue = try dict[keyPath: "value"] as! Double
  537. let thisTime = dict[keyPath: "time"] as! String
  538. let thisTimeAsSeconds = dict[keyPath: "timeAsSeconds"] as! Double
  539. let entry = basalProfileStruct(value: thisValue, time: thisTime, timeAsSeconds: thisTimeAsSeconds)
  540. basalProfile.append(entry)
  541. } catch {
  542. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: profile wrapped in quotes") }
  543. }
  544. }
  545. // Make temporary array with all values of yesterday and today
  546. let yesterdayStart = dateTimeUtils.getTimeIntervalMidnightYesterday()
  547. let todayStart = dateTimeUtils.getTimeIntervalMidnightToday()
  548. basalScheduleData.removeAll()
  549. var basal2Day: [DataStructs.basal2DayProfile] = []
  550. // Run twice to add in order yesterday then today
  551. for p in 0..<basalProfile.count {
  552. let start = yesterdayStart + basalProfile[p].timeAsSeconds
  553. var end = yesterdayStart
  554. // set the endings 1 second before the next one starts
  555. if p < basalProfile.count - 1 {
  556. end = yesterdayStart + basalProfile[p + 1].timeAsSeconds - 1
  557. } else {
  558. // set the end 1 second before midnight
  559. end = yesterdayStart + 86399
  560. }
  561. let entry = DataStructs.basal2DayProfile(basalRate: basalProfile[p].value, startDate: start, endDate: end)
  562. basal2Day.append(entry)
  563. }
  564. for p in 0..<basalProfile.count {
  565. let start = todayStart + basalProfile[p].timeAsSeconds
  566. var end = todayStart
  567. // set the endings 1 second before the next one starts
  568. if p < basalProfile.count - 1 {
  569. end = todayStart + basalProfile[p + 1].timeAsSeconds - 1
  570. } else {
  571. // set the end 1 second before midnight
  572. end = todayStart + 86399
  573. }
  574. let entry = DataStructs.basal2DayProfile(basalRate: basalProfile[p].value, startDate: start, endDate: end)
  575. basal2Day.append(entry)
  576. }
  577. let now = dateTimeUtils.nowMinus24HoursTimeInterval()
  578. for i in 0..<basal2Day.count {
  579. var timeYesterday = dateTimeUtils.getTimeInterval24HoursAgo()
  580. // we need to manually set the first one
  581. // Check that this is the first one and there are no existing entries
  582. if basalScheduleData.count == 0 {
  583. // check that the timestamp is > the current entry and < the next entry
  584. if timeYesterday >= basal2Day[i].startDate && timeYesterday < basal2Day[i].endDate {
  585. let startDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: timeYesterday + ( 60 * 5 ))
  586. basalScheduleData.append(startDot)
  587. // set the enddot where the next one will start
  588. var endDate = basal2Day[i + 1].startDate
  589. let endDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: endDate)
  590. basalScheduleData.append(endDot)
  591. }
  592. }
  593. // check if it's > 24 hours ago an <= 30 minutes from now.
  594. if basal2Day[i].startDate >= timeYesterday && basal2Day[i].startDate < dateTimeUtils.getNowTimeIntervalUTC() + ( 60 * 30 ) {
  595. let startDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: basal2Day[i].startDate)
  596. basalScheduleData.append(startDot)
  597. var endDate = basal2Day[i].endDate
  598. // if it's the last one or date is greater than now, set it to 30 minutes from now
  599. if i == basal2Day.count - 1 || endDate > dateTimeUtils.getNowTimeIntervalUTC() + ( 60 * 30 ) {
  600. endDate = dateTimeUtils.getNowTimeIntervalUTC() + ( 60 * 30 )
  601. }
  602. let endDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: endDate)
  603. basalScheduleData.append(endDot)
  604. }
  605. }
  606. if UserDefaultsRepository.graphBasal.value {
  607. updateBasalScheduledGraph()
  608. }
  609. }
  610. // NS Temp Basal Web Call
  611. func WebLoadNSTempBasals() {
  612. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Basal") }
  613. if !UserDefaultsRepository.downloadBasal.value { return }
  614. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  615. var urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  616. if token != "" {
  617. urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  618. }
  619. guard let urlData = URL(string: urlString) else {
  620. return
  621. }
  622. var request = URLRequest(url: urlData)
  623. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  624. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  625. guard error == nil else {
  626. return
  627. }
  628. guard let data = data else {
  629. return
  630. }
  631. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  632. if let json = json {
  633. DispatchQueue.main.async {
  634. self.updateBasals(entries: json)
  635. }
  636. } else {
  637. return
  638. }
  639. }
  640. task.resume()
  641. }
  642. // NS Temp Basal Response Processor
  643. func updateBasals(entries: [[String:AnyObject]]) {
  644. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") }
  645. // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
  646. basalData.removeAll()
  647. var lastEndDot = 0.0
  648. var tempArray = entries
  649. tempArray.reverse()
  650. for i in 0..<tempArray.count {
  651. let currentEntry = tempArray[i] as [String : AnyObject]?
  652. var basalDate: String
  653. if currentEntry?["timestamp"] != nil {
  654. basalDate = currentEntry?["timestamp"] as! String
  655. } else if currentEntry?["created_at"] != nil {
  656. basalDate = currentEntry?["created_at"] as! String
  657. } else {
  658. return
  659. }
  660. let strippedZone = String(basalDate.dropLast())
  661. let dateFormatter = DateFormatter()
  662. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  663. dateFormatter.locale = Locale(identifier: "en_US")
  664. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  665. let dateString = dateFormatter.date(from: strippedZone)
  666. let dateTimeStamp = dateString!.timeIntervalSince1970
  667. let basalRate = currentEntry?["absolute"] as! Double
  668. let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
  669. // Setting end dots
  670. var duration = 0.0
  671. do {
  672. duration = try currentEntry?["duration"] as! Double
  673. } catch {
  674. print("No Duration Found")
  675. }
  676. // 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
  677. if i > 0 {
  678. let priorEntry = tempArray[i - 1] as [String : AnyObject]?
  679. let priorBasalDate = priorEntry?["timestamp"] as! String
  680. let priorStrippedZone = String(priorBasalDate.dropLast())
  681. let priorDateFormatter = DateFormatter()
  682. priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  683. priorDateFormatter.locale = Locale(identifier: "en_US")
  684. priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  685. let priorDateString = dateFormatter.date(from: priorStrippedZone)
  686. let priorDateTimeStamp = priorDateString!.timeIntervalSince1970
  687. let priorDuration = priorEntry?["duration"] as! Double
  688. // if difference between time stamps is greater than the duration of the last entry, there is a gap. Give a 15 second leeway on the timestamp
  689. if Double( dateTimeStamp - priorDateTimeStamp ) > Double( (priorDuration * 60) + 15 ) {
  690. var scheduled = 0.0
  691. // cycle through basal profiles.
  692. // TODO figure out how to deal with profile changes that happen mid-gap
  693. for b in 0..<self.basalProfile.count {
  694. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  695. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  696. // check the prior temp ending to the profile seconds from midnight
  697. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeYesterday {
  698. scheduled = basalProfile[b].value
  699. }
  700. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeToday {
  701. scheduled = basalProfile[b].value
  702. }
  703. // This will iterate through from midnight on and set it for the highest matching one.
  704. }
  705. // Make the starting dot at the last ending dot
  706. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
  707. basalData.append(startDot)
  708. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Basal: Scheduled " + String(scheduled) + " " + String(dateTimeStamp)) }
  709. // Make the ending dot at the new starting dot
  710. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
  711. basalData.append(endDot)
  712. }
  713. }
  714. // Make the starting dot
  715. let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))
  716. basalData.append(startDot)
  717. // Make the ending dot
  718. // If it's the last one and has no duration, extend it for 30 minutes past the start. Otherwise set ending at duration
  719. // duration is already set to 0 if there is no duration set on it.
  720. //if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
  721. if i == tempArray.count - 1 && duration == 0.0 {
  722. lastEndDot = dateTimeStamp + (30 * 60)
  723. latestBasal = String(format:"%.2f", basalRate)
  724. } else {
  725. lastEndDot = dateTimeStamp + (duration * 60)
  726. latestBasal = String(format:"%.2f", basalRate)
  727. }
  728. // Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy
  729. if i < tempArray.count - 1 {
  730. let nextEntry = tempArray[i + 1] as [String : AnyObject]?
  731. let nextBasalDate = nextEntry?["timestamp"] as! String
  732. let nextStrippedZone = String(nextBasalDate.dropLast())
  733. let nextDateFormatter = DateFormatter()
  734. nextDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  735. nextDateFormatter.locale = Locale(identifier: "en_US")
  736. nextDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  737. let nextDateString = dateFormatter.date(from: nextStrippedZone)
  738. let nextDateTimeStamp = nextDateString!.timeIntervalSince1970
  739. if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) {
  740. lastEndDot = nextDateTimeStamp
  741. }
  742. }
  743. let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))
  744. basalData.append(endDot)
  745. }
  746. // If last basal was prior to right now, we need to create one last scheduled entry
  747. if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() {
  748. var scheduled = 0.0
  749. // cycle through basal profiles.
  750. // TODO figure out how to deal with profile changes that happen mid-gap
  751. for b in 0..<self.basalProfile.count {
  752. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  753. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  754. // check the prior temp ending to the profile seconds from midnight
  755. print("yesterday " + String(scheduleTimeYesterday))
  756. print("today " + String(scheduleTimeToday))
  757. if lastEndDot >= scheduleTimeToday {
  758. scheduled = basalProfile[b].value
  759. }
  760. }
  761. latestBasal = String(format:"%.2f", scheduled)
  762. // Make the starting dot at the last ending dot
  763. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
  764. basalData.append(startDot)
  765. // Make the ending dot 10 minutes after now
  766. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970 + (60 * 10)))
  767. basalData.append(endDot)
  768. }
  769. tableData[2].value = latestBasal
  770. if UserDefaultsRepository.graphBasal.value {
  771. updateBasalGraph()
  772. }
  773. }
  774. // NS Bolus Web Call
  775. func webLoadNSBoluses(){
  776. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Bolus") }
  777. if !UserDefaultsRepository.downloadBolus.value { return }
  778. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  779. let urlUser = UserDefaultsRepository.url.value
  780. var searchString = "find[eventType]=Correction%20Bolus&find[created_at][$gte]=" + yesterdayString
  781. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  782. if token == "" {
  783. urlDataPath = urlDataPath + searchString
  784. }
  785. else
  786. {
  787. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  788. }
  789. guard let urlData = URL(string: urlDataPath) else {
  790. return
  791. }
  792. var request = URLRequest(url: urlData)
  793. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  794. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  795. guard error == nil else {
  796. return
  797. }
  798. guard let data = data else {
  799. return
  800. }
  801. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  802. if let json = json {
  803. DispatchQueue.main.async {
  804. self.processNSBolus(entries: json)
  805. }
  806. } else {
  807. return
  808. }
  809. }
  810. task.resume()
  811. }
  812. // NS Meal Bolus Response Processor
  813. func processNSBolus(entries: [[String:AnyObject]]) {
  814. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Bolus") }
  815. // because it's a small array, we're going to destroy and reload every time.
  816. bolusData.removeAll()
  817. var lastFoundIndex = 0
  818. for i in 0..<entries.count {
  819. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  820. var bolusDate: String
  821. if currentEntry?["timestamp"] != nil {
  822. bolusDate = currentEntry?["timestamp"] as! String
  823. } else if currentEntry?["created_at"] != nil {
  824. bolusDate = currentEntry?["created_at"] as! String
  825. } else {
  826. return
  827. }
  828. let strippedZone = String(bolusDate.dropLast())
  829. let dateFormatter = DateFormatter()
  830. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  831. dateFormatter.locale = Locale(identifier: "en_US")
  832. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  833. let dateString = dateFormatter.date(from: strippedZone)
  834. let dateTimeStamp = dateString!.timeIntervalSince1970
  835. do {
  836. let bolus = try currentEntry?["insulin"] as! Double
  837. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  838. lastFoundIndex = sgv.foundIndex
  839. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  840. // Make the dot
  841. let dot = bolusCarbGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  842. bolusData.append(dot)
  843. }
  844. } catch {
  845. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Bolus") }
  846. }
  847. }
  848. if UserDefaultsRepository.graphBolus.value {
  849. updateBolusGraph()
  850. }
  851. }
  852. // NS Carb Web Call
  853. func webLoadNSCarbs(){
  854. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Carbs") }
  855. if !UserDefaultsRepository.downloadCarbs.value { return }
  856. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  857. let urlUser = UserDefaultsRepository.url.value
  858. var searchString = "find[eventType]=Meal%20Bolus&find[created_at][$gte]=" + yesterdayString
  859. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  860. if token == "" {
  861. urlDataPath = urlDataPath + searchString
  862. }
  863. else
  864. {
  865. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  866. }
  867. guard let urlData = URL(string: urlDataPath) else {
  868. return
  869. }
  870. var request = URLRequest(url: urlData)
  871. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  872. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  873. guard error == nil else {
  874. return
  875. }
  876. guard let data = data else {
  877. return
  878. }
  879. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  880. if let json = json {
  881. DispatchQueue.main.async {
  882. self.processNSCarbs(entries: json)
  883. }
  884. } else {
  885. return
  886. }
  887. }
  888. task.resume()
  889. }
  890. // NS Carb Bolus Response Processor
  891. func processNSCarbs(entries: [[String:AnyObject]]) {
  892. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Carbs") }
  893. // because it's a small array, we're going to destroy and reload every time.
  894. carbData.removeAll()
  895. var lastFoundIndex = 0
  896. for i in 0..<entries.count {
  897. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  898. var carbDate: String
  899. if currentEntry?["timestamp"] != nil {
  900. carbDate = currentEntry?["timestamp"] as! String
  901. } else if currentEntry?["created_at"] != nil {
  902. carbDate = currentEntry?["created_at"] as! String
  903. } else {
  904. return
  905. }
  906. let strippedZone = String(carbDate.dropLast())
  907. let dateFormatter = DateFormatter()
  908. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  909. dateFormatter.locale = Locale(identifier: "en_US")
  910. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  911. let dateString = dateFormatter.date(from: strippedZone)
  912. let dateTimeStamp = dateString!.timeIntervalSince1970
  913. do {
  914. let carbs = try currentEntry?["carbs"] as! Double
  915. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  916. lastFoundIndex = sgv.foundIndex
  917. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  918. // Make the dot
  919. let dot = bolusCarbGraphStruct(value: carbs, date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  920. carbData.append(dot)
  921. }
  922. } catch {
  923. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Carb entry") }
  924. }
  925. }
  926. if UserDefaultsRepository.graphCarbs.value {
  927. updateCarbGraph()
  928. }
  929. }
  930. }