NightScout.swift 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  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. //NS Cage Struct
  12. struct cageData: Codable {
  13. var created_at: String
  14. }
  15. //NS Basal Profile Struct
  16. struct basalProfileStruct: Codable {
  17. var value: Double
  18. var time: String
  19. var timeAsSeconds: Double
  20. }
  21. //NS Basal Data Struct
  22. struct basalGraphStruct: Codable {
  23. var basalRate: Double
  24. var date: TimeInterval
  25. }
  26. //NS Bolus Data Struct
  27. struct bolusCarbGraphStruct: Codable {
  28. var value: Double
  29. var date: TimeInterval
  30. var sgv: Int
  31. }
  32. // Main loader for all data
  33. func nightscoutLoader(forceLoad: Bool = false) {
  34. var needsLoaded: Bool = false
  35. var staleData: Bool = false
  36. var onlyPullLastRecord = false
  37. // If we have existing data and it's within 5 minutes, we aren't going to do a BG network call
  38. // if we have stale BG data 10 min or older, we're only going to attempt to pull BG and Loop status
  39. // to not have a full refresh every 15 seconds. The remaining data will start pulling again on the
  40. // next BG reading that comes in.
  41. if bgData.count > 0 {
  42. let now = NSDate().timeIntervalSince1970
  43. let lastReadingTime = bgData[bgData.count - 1].date
  44. let secondsAgo = now - lastReadingTime
  45. if secondsAgo >= 5*60 {
  46. needsLoaded = true
  47. if secondsAgo < 10*60 {
  48. onlyPullLastRecord = true
  49. } else {
  50. staleData = true
  51. }
  52. }
  53. } else {
  54. needsLoaded = true
  55. }
  56. if forceLoad { needsLoaded = true}
  57. // Only update if we don't have a current reading or forced to load
  58. if needsLoaded {
  59. if UserDefaultsRepository.url.value != "" {
  60. webLoadNSDeviceStatus()
  61. }
  62. if UserDefaultsRepository.shareUserName.value != "" && UserDefaultsRepository.sharePassword.value != "" {
  63. webLoadDexShare(onlyPullLastRecord: onlyPullLastRecord)
  64. } else {
  65. webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  66. }
  67. if !staleData && UserDefaultsRepository.url.value != "" {
  68. webLoadNSProfile()
  69. if UserDefaultsRepository.downloadBasal.value {
  70. WebLoadNSTempBasals()
  71. }
  72. if UserDefaultsRepository.downloadBolus.value {
  73. webLoadNSBoluses()
  74. }
  75. if UserDefaultsRepository.downloadCarbs.value {
  76. webLoadNSCarbs()
  77. }
  78. webLoadNSCage()
  79. webLoadNSSage()
  80. }
  81. // Give the alarms and calendar 15 seconds delay to allow time for data to compile
  82. self.startViewTimer(time: viewTimeInterval)
  83. } else {
  84. if bgData.count > 0 {
  85. self.checkAlarms(bgs: bgData)
  86. }
  87. // Used for Min Ago watch readings
  88. writeCalendar()
  89. if UserDefaultsRepository.url.value != "" {
  90. if latestLoopTime == 0 {
  91. webLoadNSDeviceStatus()
  92. webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  93. webLoadNSProfile()
  94. if UserDefaultsRepository.downloadBasal.value {
  95. WebLoadNSTempBasals()
  96. }
  97. if UserDefaultsRepository.downloadBolus.value {
  98. webLoadNSBoluses()
  99. }
  100. if UserDefaultsRepository.downloadCarbs.value {
  101. webLoadNSCarbs()
  102. }
  103. webLoadNSCage()
  104. webLoadNSSage()
  105. }
  106. }
  107. }
  108. }
  109. // Dex Share Web Call
  110. func webLoadDexShare(onlyPullLastRecord: Bool = false) {
  111. var count = 288
  112. if onlyPullLastRecord { count = 1 }
  113. dexShare?.fetchData(count) { (err, result) -> () in
  114. // TODO: add error checking
  115. if(err == nil) {
  116. var data = result!
  117. self.ProcessNSBGData(data: data, onlyPullLastRecord: onlyPullLastRecord)
  118. } else {
  119. // If we get an error, immediately try to pull NS BG Data
  120. self.webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  121. }
  122. }
  123. }
  124. // NS BG Data Web call
  125. func webLoadNSBGData(onlyPullLastRecord: Bool = false) {
  126. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: BG") }
  127. // Set the count= in the url either to pull 24 hours or only the last record
  128. var points = "1"
  129. if !onlyPullLastRecord {
  130. points = String(self.graphHours * 12 + 1)
  131. }
  132. // URL processor
  133. var urlBGDataPath: String = UserDefaultsRepository.url.value + "/api/v1/entries/sgv.json?"
  134. if token == "" {
  135. urlBGDataPath = urlBGDataPath + "count=" + points
  136. } else {
  137. urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
  138. }
  139. guard let urlBGData = URL(string: urlBGDataPath) else {
  140. return
  141. }
  142. var request = URLRequest(url: urlBGData)
  143. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  144. // Downloader
  145. let getBGTask = URLSession.shared.dataTask(with: request) { data, response, error in
  146. guard error == nil else {
  147. return
  148. }
  149. guard let data = data else {
  150. return
  151. }
  152. let decoder = JSONDecoder()
  153. let entriesResponse = try? decoder.decode([ShareGlucoseData].self, from: data)
  154. if let entriesResponse = entriesResponse {
  155. DispatchQueue.main.async {
  156. // trigger the processor for the data after downloading.
  157. self.ProcessNSBGData(data: entriesResponse, onlyPullLastRecord: onlyPullLastRecord, isNS: true)
  158. }
  159. } else {
  160. return
  161. }
  162. }
  163. getBGTask.resume()
  164. }
  165. // NS BG Data Response processor
  166. func ProcessNSBGData(data: [ShareGlucoseData], onlyPullLastRecord: Bool, isNS: Bool = false){
  167. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: BG") }
  168. var pullDate = data[data.count - 1].date
  169. if isNS {
  170. pullDate = data[data.count - 1].date / 1000
  171. pullDate.round(FloatingPointRoundingRule.toNearestOrEven)
  172. }
  173. var latestDate = data[0].date
  174. if isNS {
  175. latestDate = data[0].date / 1000
  176. latestDate.round(FloatingPointRoundingRule.toNearestOrEven)
  177. }
  178. let now = dateTimeUtils.getNowTimeIntervalUTC()
  179. if !isNS && (latestDate + 330) < now {
  180. webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  181. return
  182. }
  183. // 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
  184. if !onlyPullLastRecord {
  185. bgData.removeAll()
  186. } else if bgData[bgData.count - 1].date != pullDate {
  187. bgData.removeFirst()
  188. if data.count > 0 && UserDefaultsRepository.speakBG.value {
  189. speakBG(sgv: data[data.count - 1].sgv)
  190. }
  191. } else {
  192. if data.count > 0 {
  193. self.updateBadge(val: data[data.count - 1].sgv)
  194. }
  195. // self.viewUpdateNSBG()
  196. return
  197. }
  198. // 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.
  199. for i in 0..<data.count{
  200. var dateString = data[data.count - 1 - i].date
  201. if isNS {
  202. dateString = data[data.count - 1 - i].date / 1000
  203. dateString.round(FloatingPointRoundingRule.toNearestOrEven)
  204. }
  205. if dateString >= dateTimeUtils.getTimeInterval24HoursAgo() {
  206. let reading = ShareGlucoseData(sgv: data[data.count - 1 - i].sgv, date: dateString, direction: data[data.count - 1 - i].direction)
  207. bgData.append(reading)
  208. }
  209. }
  210. viewUpdateNSBG(isNS: isNS)
  211. }
  212. // NS BG Data Front end updater
  213. func viewUpdateNSBG (isNS: Bool) {
  214. DispatchQueue.main.async {
  215. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Display: BG") }
  216. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  217. let entries = self.bgData
  218. if entries.count > 0 {
  219. let latestEntryi = entries.count - 1
  220. let latestBG = entries[latestEntryi].sgv
  221. let priorBG = entries[latestEntryi - 1].sgv
  222. let deltaBG = latestBG - priorBG as Int
  223. let lastBGTime = entries[latestEntryi].date
  224. let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-lastBGTime) / 60
  225. var userUnit = " mg/dL"
  226. if self.mmol {
  227. userUnit = " mmol/L"
  228. }
  229. // TODO: remove testing feature to color code arrow based on NS vs Dex
  230. if isNS {
  231. self.serverText.text = "Nightscout"
  232. } else {
  233. self.serverText.text = "Dexcom"
  234. }
  235. self.BGText.text = bgUnits.toDisplayUnits(String(latestBG))
  236. snoozer.BGLabel.text = bgUnits.toDisplayUnits(String(latestBG))
  237. self.setBGTextColor()
  238. if let directionBG = entries[latestEntryi].direction {
  239. self.DirectionText.text = self.bgDirectionGraphic(directionBG)
  240. snoozer.DirectionLabel.text = self.bgDirectionGraphic(directionBG)
  241. self.latestDirectionString = self.bgDirectionGraphic(directionBG)
  242. }
  243. else
  244. {
  245. self.DirectionText.text = ""
  246. snoozer.DirectionLabel.text = ""
  247. self.latestDirectionString = ""
  248. }
  249. if deltaBG < 0 {
  250. self.DeltaText.text = bgUnits.toDisplayUnits(String(deltaBG))
  251. snoozer.DeltaLabel.text = bgUnits.toDisplayUnits(String(deltaBG))
  252. self.latestDeltaString = String(deltaBG)
  253. }
  254. else
  255. {
  256. self.DeltaText.text = "+" + bgUnits.toDisplayUnits(String(deltaBG))
  257. snoozer.DeltaLabel.text = "+" + bgUnits.toDisplayUnits(String(deltaBG))
  258. self.latestDeltaString = "+" + String(deltaBG)
  259. }
  260. self.updateBadge(val: latestBG)
  261. }
  262. else
  263. {
  264. return
  265. }
  266. self.updateBGGraph()
  267. self.updateMinAgo()
  268. self.updateStats()
  269. }
  270. }
  271. // NS Device Status Web Call
  272. func webLoadNSDeviceStatus() {
  273. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: device status") }
  274. let urlUser = UserDefaultsRepository.url.value
  275. // NS Api is not working to find by greater than date
  276. var urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=288"
  277. if token != "" {
  278. urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=288&token=" + token
  279. }
  280. let escapedAddress = urlStringDeviceStatus.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  281. guard let urlDeviceStatus = URL(string: escapedAddress!) else {
  282. return
  283. }
  284. var requestDeviceStatus = URLRequest(url: urlDeviceStatus)
  285. requestDeviceStatus.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  286. let deviceStatusTask = URLSession.shared.dataTask(with: requestDeviceStatus) { data, response, error in
  287. guard error == nil else {
  288. return
  289. }
  290. guard let data = data else {
  291. return
  292. }
  293. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  294. if let json = json {
  295. DispatchQueue.main.async {
  296. self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
  297. }
  298. } else {
  299. return
  300. } }
  301. deviceStatusTask.resume()
  302. }
  303. // NS Device Status Response Processor
  304. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  305. self.clearLastInfoData(index: 0)
  306. self.clearLastInfoData(index: 1)
  307. self.clearLastInfoData(index: 3)
  308. self.clearLastInfoData(index: 4)
  309. self.clearLastInfoData(index: 5)
  310. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
  311. if jsonDeviceStatus.count == 0 {
  312. return
  313. }
  314. //Process the current data first
  315. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  316. //pump and uploader
  317. let formatter = ISO8601DateFormatter()
  318. formatter.formatOptions = [.withFullDate,
  319. .withTime,
  320. .withDashSeparatorInDate,
  321. .withColonSeparatorInTime]
  322. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  323. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  324. if let reservoirData = lastPumpRecord["reservoir"] as? Double {
  325. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  326. } else {
  327. tableData[5].value = "50+U"
  328. }
  329. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  330. let upbat = uploader["battery"] as! Double
  331. tableData[4].value = String(format:"%.0f", upbat) + "%"
  332. }
  333. }
  334. }
  335. // Loop
  336. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  337. print("Loop: \(lastLoopRecord)")
  338. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  339. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  340. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  341. if let failure = lastLoopRecord["failureReason"] {
  342. LoopStatusLabel.text = "X"
  343. latestLoopStatusString = "X"
  344. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  345. } else {
  346. var wasEnacted = false
  347. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  348. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  349. wasEnacted = true
  350. if let lastTempBasal = enacted["rate"] as? Double {
  351. }
  352. }
  353. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  354. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  355. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  356. }
  357. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  358. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  359. latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
  360. }
  361. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  362. let prediction = predictdata["values"] as! [Int]
  363. PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
  364. PredictionLabel.textColor = UIColor.systemPurple
  365. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  366. predictionData.removeAll()
  367. var predictionTime = lastLoopTime + 300
  368. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  369. var i = 1
  370. while i <= toLoad {
  371. if i < prediction.count {
  372. let prediction = ShareGlucoseData(sgv: prediction[i], date: predictionTime, direction: "flat")
  373. predictionData.append(prediction)
  374. predictionTime += 300
  375. }
  376. i += 1
  377. }
  378. }
  379. let predMin = prediction.min()
  380. let predMax = prediction.max()
  381. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  382. if UserDefaultsRepository.graphPrediction.value {
  383. updatePredictionGraph()
  384. }
  385. }
  386. if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
  387. tableData[8].value = String(format:"%.2fU", recBolus)
  388. }
  389. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  390. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  391. var lastBGTime = lastLoopTime
  392. if bgData.count > 0 {
  393. lastBGTime = bgData[bgData.count - 1].date
  394. }
  395. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  396. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  397. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  398. if tempBasalTime > lastBGTime && !wasEnacted {
  399. LoopStatusLabel.text = "⏀"
  400. latestLoopStatusString = "⏀"
  401. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  402. } else {
  403. LoopStatusLabel.text = "↻"
  404. latestLoopStatusString = "↻"
  405. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  406. }
  407. }
  408. } else {
  409. LoopStatusLabel.text = "↻"
  410. latestLoopStatusString = "↻"
  411. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  412. }
  413. }
  414. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 {
  415. LoopStatusLabel.text = "⚠"
  416. latestLoopStatusString = "⚠"
  417. }
  418. latestLoopTime = lastLoopTime
  419. } // end lastLoopTime
  420. } // end lastLoop Record
  421. var oText = "" as String
  422. currentOverride = 1.0
  423. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  424. if let lastOverrideTime = formatter.date(from: (lastOverride["timestamp"] as! String))?.timeIntervalSince1970 {
  425. }
  426. if lastOverride["active"] as! Bool {
  427. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  428. if let multiplier = lastOverride["multiplier"] as? Double {
  429. currentOverride = multiplier
  430. oText += String(format: "%.0f%%", (multiplier * 100))
  431. }
  432. else
  433. {
  434. oText += String(format:"%.0f%%", 100)
  435. }
  436. oText += " ("
  437. let minValue = lastCorrection["minValue"] as! Double
  438. let maxValue = lastCorrection["maxValue"] as! Double
  439. oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
  440. tableData[3].value = oText
  441. }
  442. }
  443. infoTable.reloadData()
  444. // Process Override Data
  445. overrideData.removeAll()
  446. for i in 0..<jsonDeviceStatus.count {
  447. let deviceStatus = jsonDeviceStatus[i] as [String : AnyObject]?
  448. if let override = deviceStatus?["override"] as! [String : AnyObject]? {
  449. let formatter = ISO8601DateFormatter()
  450. formatter.formatOptions = [.withFullDate,
  451. .withTime,
  452. .withDashSeparatorInDate,
  453. .withColonSeparatorInTime]
  454. if let timestamp = formatter.date(from: (override["timestamp"] as! String))?.timeIntervalSince1970 {
  455. if timestamp > dateTimeUtils.getTimeInterval24HoursAgo() {
  456. if let isActive = override["active"] as? Bool {
  457. if isActive {
  458. if let multiplier = override["multiplier"] as? Double {
  459. let override = DataStructs.overrideGraphStruct(value: multiplier, date: timestamp, sgv: Int(UserDefaultsRepository.overrideDisplayLocation.value))
  460. overrideData.append(override)
  461. }
  462. } else {
  463. let multiplier = 1.0 as Double
  464. let override = DataStructs.overrideGraphStruct(value: multiplier, date: timestamp, sgv: Int(UserDefaultsRepository.overrideDisplayLocation.value))
  465. overrideData.append(override)
  466. }
  467. }
  468. }
  469. }
  470. }
  471. }
  472. overrideData.reverse()
  473. updateOverrideGraph()
  474. checkOverrideAlarms()
  475. }
  476. // NS Cage Web Call
  477. func webLoadNSCage() {
  478. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: CAGE") }
  479. let urlUser = UserDefaultsRepository.url.value
  480. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Site%20Change&count=1"
  481. if token != "" {
  482. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Site%20Change&count=1"
  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 decoder = JSONDecoder()
  497. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  498. if let entriesResponse = entriesResponse {
  499. DispatchQueue.main.async {
  500. self.updateCage(data: entriesResponse)
  501. }
  502. } else {
  503. return
  504. }
  505. }
  506. task.resume()
  507. }
  508. // NS Cage Response Processor
  509. func updateCage(data: [cageData]) {
  510. self.clearLastInfoData(index: 7)
  511. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: CAGE") }
  512. if data.count == 0 {
  513. return
  514. }
  515. let lastCageString = data[0].created_at
  516. let formatter = ISO8601DateFormatter()
  517. formatter.formatOptions = [.withFullDate,
  518. .withTime,
  519. .withDashSeparatorInDate,
  520. .withColonSeparatorInTime]
  521. UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval
  522. if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 {
  523. let now = NSDate().timeIntervalSince1970
  524. let secondsAgo = now - cageTime
  525. //let days = 24 * 60 * 60
  526. let formatter = DateComponentsFormatter()
  527. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  528. formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string
  529. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  530. let formattedDuration = formatter.string(from: secondsAgo)
  531. tableData[7].value = formattedDuration ?? ""
  532. }
  533. infoTable.reloadData()
  534. }
  535. // NS Sage Web Call
  536. func webLoadNSSage() {
  537. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: SAGE") }
  538. let lastDateString = dateTimeUtils.nowMinus10DaysTimeInterval()
  539. let urlUser = UserDefaultsRepository.url.value
  540. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  541. if token != "" {
  542. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  543. }
  544. guard let urlData = URL(string: urlString) else {
  545. return
  546. }
  547. var request = URLRequest(url: urlData)
  548. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  549. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  550. guard error == nil else {
  551. return
  552. }
  553. guard let data = data else {
  554. return
  555. }
  556. let decoder = JSONDecoder()
  557. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  558. if let entriesResponse = entriesResponse {
  559. DispatchQueue.main.async {
  560. self.updateSage(data: entriesResponse)
  561. }
  562. } else {
  563. return
  564. }
  565. }
  566. task.resume()
  567. }
  568. // NS Sage Response Processor
  569. func updateSage(data: [cageData]) {
  570. self.clearLastInfoData(index: 6)
  571. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process/Display: SAGE") }
  572. if data.count == 0 {
  573. return
  574. }
  575. var lastSageString = data[0].created_at
  576. let formatter = ISO8601DateFormatter()
  577. formatter.formatOptions = [.withFullDate,
  578. .withTime,
  579. .withDashSeparatorInDate,
  580. .withColonSeparatorInTime]
  581. UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: (lastSageString))?.timeIntervalSince1970 as! TimeInterval
  582. if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 {
  583. let now = NSDate().timeIntervalSince1970
  584. let secondsAgo = now - sageTime
  585. let days = 24 * 60 * 60
  586. let formatter = DateComponentsFormatter()
  587. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  588. formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
  589. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  590. let formattedDuration = formatter.string(from: secondsAgo)
  591. tableData[6].value = formattedDuration ?? ""
  592. }
  593. infoTable.reloadData()
  594. }
  595. // NS Profile Web Call
  596. func webLoadNSProfile() {
  597. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: profile") }
  598. let urlUser = UserDefaultsRepository.url.value
  599. var urlString = urlUser + "/api/v1/profile/current.json"
  600. if token != "" {
  601. urlString = urlUser + "/api/v1/profile/current.json?token=" + token
  602. }
  603. let escapedAddress = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  604. guard let url = URL(string: escapedAddress!) else {
  605. return
  606. }
  607. var request = URLRequest(url: url)
  608. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  609. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  610. guard error == nil else {
  611. return
  612. }
  613. guard let data = data else {
  614. return
  615. }
  616. let json = try? JSONSerialization.jsonObject(with: data) as! Dictionary<String, Any>
  617. if let json = json {
  618. DispatchQueue.main.async {
  619. self.updateProfile(jsonDeviceStatus: json)
  620. }
  621. } else {
  622. return
  623. }
  624. }
  625. task.resume()
  626. }
  627. // NS Profile Response Processor
  628. func updateProfile(jsonDeviceStatus: Dictionary<String, Any>) {
  629. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: profile") }
  630. if jsonDeviceStatus.count == 0 {
  631. return
  632. }
  633. if jsonDeviceStatus[keyPath: "message"] != nil { return }
  634. let basal = try jsonDeviceStatus[keyPath: "store.Default.basal"] as! NSArray
  635. basalProfile.removeAll()
  636. for i in 0..<basal.count {
  637. let dict = basal[i] as! Dictionary<String, Any>
  638. do {
  639. let thisValue = try dict[keyPath: "value"] as! Double
  640. let thisTime = dict[keyPath: "time"] as! String
  641. let thisTimeAsSeconds = dict[keyPath: "timeAsSeconds"] as! Double
  642. let entry = basalProfileStruct(value: thisValue, time: thisTime, timeAsSeconds: thisTimeAsSeconds)
  643. basalProfile.append(entry)
  644. } catch {
  645. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: profile wrapped in quotes") }
  646. }
  647. }
  648. // Don't process the basal or draw the graph until after the BG has been fully processeed and drawn
  649. if firstGraphLoad { return }
  650. // Make temporary array with all values of yesterday and today
  651. let yesterdayStart = dateTimeUtils.getTimeIntervalMidnightYesterday()
  652. let todayStart = dateTimeUtils.getTimeIntervalMidnightToday()
  653. var basal2Day: [DataStructs.basal2DayProfile] = []
  654. // Run twice to add in order yesterday then today.
  655. for p in 0..<basalProfile.count {
  656. let start = yesterdayStart + basalProfile[p].timeAsSeconds
  657. var end = yesterdayStart
  658. // set the endings 1 second before the next one starts
  659. if p < basalProfile.count - 1 {
  660. end = yesterdayStart + basalProfile[p + 1].timeAsSeconds - 1
  661. } else {
  662. // set the end 1 second before midnight
  663. end = yesterdayStart + 86399
  664. }
  665. let entry = DataStructs.basal2DayProfile(basalRate: basalProfile[p].value, startDate: start, endDate: end)
  666. basal2Day.append(entry)
  667. }
  668. for p in 0..<basalProfile.count {
  669. let start = todayStart + basalProfile[p].timeAsSeconds
  670. var end = todayStart
  671. // set the endings 1 second before the next one starts
  672. if p < basalProfile.count - 1 {
  673. end = todayStart + basalProfile[p + 1].timeAsSeconds - 1
  674. } else {
  675. // set the end 1 second before midnight
  676. end = todayStart + 86399
  677. }
  678. let entry = DataStructs.basal2DayProfile(basalRate: basalProfile[p].value, startDate: start, endDate: end)
  679. basal2Day.append(entry)
  680. }
  681. let now = dateTimeUtils.nowMinus24HoursTimeInterval()
  682. var firstPass = true
  683. basalScheduleData.removeAll()
  684. for i in 0..<basal2Day.count {
  685. var timeYesterday = dateTimeUtils.getTimeInterval24HoursAgo()
  686. // This processed everything after the first one.
  687. if firstPass == false
  688. && basal2Day[i].startDate <= dateTimeUtils.getNowTimeIntervalUTC() {
  689. let startDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: basal2Day[i].startDate)
  690. basalScheduleData.append(startDot)
  691. var endDate = basal2Day[i].endDate
  692. // if it's the last one in the profile or date is greater than now, set it to the last BG dot
  693. if i == basal2Day.count - 1 || endDate > dateTimeUtils.getNowTimeIntervalUTC() {
  694. endDate = Double(dateTimeUtils.getNowTimeIntervalUTC())
  695. }
  696. let endDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: endDate)
  697. basalScheduleData.append(endDot)
  698. }
  699. // we need to manually set the first one
  700. // Check that this is the first one and there are no existing entries
  701. if firstPass == true {
  702. // check that the timestamp is > the current entry and < the next entry
  703. if timeYesterday >= basal2Day[i].startDate && timeYesterday < basal2Day[i].endDate {
  704. // Set the start time to match the BG start
  705. let startDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: Double(dateTimeUtils.getTimeInterval24HoursAgo() + (60 * 5)))
  706. basalScheduleData.append(startDot)
  707. // set the enddot where the next one will start
  708. var endDate = basal2Day[i].endDate
  709. let endDot = basalGraphStruct(basalRate: basal2Day[i].basalRate, date: endDate)
  710. basalScheduleData.append(endDot)
  711. firstPass = false
  712. }
  713. }
  714. }
  715. if UserDefaultsRepository.graphBasal.value {
  716. updateBasalScheduledGraph()
  717. }
  718. }
  719. // NS Temp Basal Web Call
  720. func WebLoadNSTempBasals() {
  721. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Basal") }
  722. if !UserDefaultsRepository.downloadBasal.value { return }
  723. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  724. var urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  725. if token != "" {
  726. urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?token=" + token + "&find[eventType][$eq]=Temp%20Basal&find[created_at][$gte]=" + yesterdayString
  727. }
  728. guard let urlData = URL(string: urlString) else {
  729. return
  730. }
  731. var request = URLRequest(url: urlData)
  732. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  733. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  734. guard error == nil else {
  735. return
  736. }
  737. guard let data = data else {
  738. return
  739. }
  740. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  741. if let json = json {
  742. DispatchQueue.main.async {
  743. self.updateBasals(entries: json)
  744. }
  745. } else {
  746. return
  747. }
  748. }
  749. task.resume()
  750. }
  751. // NS Temp Basal Response Processor
  752. func updateBasals(entries: [[String:AnyObject]]) {
  753. self.clearLastInfoData(index: 2)
  754. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") }
  755. // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
  756. basalData.removeAll()
  757. var lastEndDot = 0.0
  758. var tempArray = entries
  759. tempArray.reverse()
  760. for i in 0..<tempArray.count {
  761. let currentEntry = tempArray[i] as [String : AnyObject]?
  762. var basalDate: String
  763. if currentEntry?["timestamp"] != nil {
  764. basalDate = currentEntry?["timestamp"] as! String
  765. } else if currentEntry?["created_at"] != nil {
  766. basalDate = currentEntry?["created_at"] as! String
  767. } else {
  768. return
  769. }
  770. let strippedZone = String(basalDate.dropLast())
  771. let dateFormatter = DateFormatter()
  772. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  773. dateFormatter.locale = Locale(identifier: "en_US")
  774. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  775. let dateString = dateFormatter.date(from: strippedZone)
  776. let dateTimeStamp = dateString!.timeIntervalSince1970
  777. let basalRate = currentEntry?["absolute"] as! Double
  778. let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
  779. // Setting end dots
  780. var duration = 0.0
  781. do {
  782. duration = try currentEntry?["duration"] as! Double
  783. } catch {
  784. print("No Duration Found")
  785. }
  786. // 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
  787. if i > 0 {
  788. let priorEntry = tempArray[i - 1] as [String : AnyObject]?
  789. let priorBasalDate = priorEntry?["timestamp"] as! String
  790. let priorStrippedZone = String(priorBasalDate.dropLast())
  791. let priorDateFormatter = DateFormatter()
  792. priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  793. priorDateFormatter.locale = Locale(identifier: "en_US")
  794. priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  795. let priorDateString = dateFormatter.date(from: priorStrippedZone)
  796. let priorDateTimeStamp = priorDateString!.timeIntervalSince1970
  797. let priorDuration = priorEntry?["duration"] as! Double
  798. // 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
  799. if Double( dateTimeStamp - priorDateTimeStamp ) > Double( (priorDuration * 60) + 15 ) {
  800. var scheduled = 0.0
  801. // cycle through basal profiles.
  802. // TODO figure out how to deal with profile changes that happen mid-gap
  803. for b in 0..<self.basalProfile.count {
  804. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  805. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  806. // check the prior temp ending to the profile seconds from midnight
  807. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeYesterday {
  808. scheduled = basalProfile[b].value
  809. }
  810. if (priorDateTimeStamp + (priorDuration * 60)) >= scheduleTimeToday {
  811. scheduled = basalProfile[b].value
  812. }
  813. // This will iterate through from midnight on and set it for the highest matching one.
  814. }
  815. // Make the starting dot at the last ending dot
  816. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
  817. basalData.append(startDot)
  818. //if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Basal: Scheduled " + String(scheduled) + " " + String(dateTimeStamp)) }
  819. // Make the ending dot at the new starting dot
  820. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
  821. basalData.append(endDot)
  822. }
  823. }
  824. // Make the starting dot
  825. let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))
  826. basalData.append(startDot)
  827. // Make the ending dot
  828. // If it's the last one and has no duration, extend it for 30 minutes past the start. Otherwise set ending at duration
  829. // duration is already set to 0 if there is no duration set on it.
  830. //if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
  831. if i == tempArray.count - 1 && duration == 0.0 {
  832. lastEndDot = dateTimeStamp + (30 * 60)
  833. latestBasal = String(format:"%.2f", basalRate)
  834. } else {
  835. lastEndDot = dateTimeStamp + (duration * 60)
  836. latestBasal = String(format:"%.2f", basalRate)
  837. }
  838. // Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy
  839. if i < tempArray.count - 1 {
  840. let nextEntry = tempArray[i + 1] as [String : AnyObject]?
  841. let nextBasalDate = nextEntry?["timestamp"] as! String
  842. let nextStrippedZone = String(nextBasalDate.dropLast())
  843. let nextDateFormatter = DateFormatter()
  844. nextDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  845. nextDateFormatter.locale = Locale(identifier: "en_US")
  846. nextDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  847. let nextDateString = dateFormatter.date(from: nextStrippedZone)
  848. let nextDateTimeStamp = nextDateString!.timeIntervalSince1970
  849. if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) {
  850. lastEndDot = nextDateTimeStamp
  851. }
  852. }
  853. let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))
  854. basalData.append(endDot)
  855. }
  856. // If last basal was prior to right now, we need to create one last scheduled entry
  857. if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() {
  858. var scheduled = 0.0
  859. // cycle through basal profiles.
  860. // TODO figure out how to deal with profile changes that happen mid-gap
  861. for b in 0..<self.basalProfile.count {
  862. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  863. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  864. // check the prior temp ending to the profile seconds from midnight
  865. print("yesterday " + String(scheduleTimeYesterday))
  866. print("today " + String(scheduleTimeToday))
  867. if lastEndDot >= scheduleTimeToday {
  868. scheduled = basalProfile[b].value
  869. }
  870. }
  871. latestBasal = String(format:"%.2f", scheduled)
  872. // Make the starting dot at the last ending dot
  873. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
  874. basalData.append(startDot)
  875. // Make the ending dot 10 minutes after now
  876. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970 + (60 * 10)))
  877. basalData.append(endDot)
  878. }
  879. tableData[2].value = latestBasal
  880. infoTable.reloadData()
  881. if UserDefaultsRepository.graphBasal.value {
  882. updateBasalGraph()
  883. }
  884. infoTable.reloadData()
  885. }
  886. // NS Bolus Web Call
  887. func webLoadNSBoluses(){
  888. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Bolus") }
  889. if !UserDefaultsRepository.downloadBolus.value { return }
  890. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  891. let urlUser = UserDefaultsRepository.url.value
  892. var searchString = "find[eventType]=Correction%20Bolus&find[created_at][$gte]=" + yesterdayString
  893. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  894. if token == "" {
  895. urlDataPath = urlDataPath + searchString
  896. }
  897. else
  898. {
  899. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  900. }
  901. guard let urlData = URL(string: urlDataPath) else {
  902. return
  903. }
  904. var request = URLRequest(url: urlData)
  905. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  906. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  907. guard error == nil else {
  908. return
  909. }
  910. guard let data = data else {
  911. return
  912. }
  913. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  914. if let json = json {
  915. DispatchQueue.main.async {
  916. self.processNSBolus(entries: json)
  917. }
  918. } else {
  919. return
  920. }
  921. }
  922. task.resume()
  923. }
  924. // NS Meal Bolus Response Processor
  925. func processNSBolus(entries: [[String:AnyObject]]) {
  926. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Bolus") }
  927. // because it's a small array, we're going to destroy and reload every time.
  928. bolusData.removeAll()
  929. var lastFoundIndex = 0
  930. for i in 0..<entries.count {
  931. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  932. var bolusDate: String
  933. if currentEntry?["timestamp"] != nil {
  934. bolusDate = currentEntry?["timestamp"] as! String
  935. } else if currentEntry?["created_at"] != nil {
  936. bolusDate = currentEntry?["created_at"] as! String
  937. } else {
  938. return
  939. }
  940. // fix to remove millisecond (after period in timestamp) for FreeAPS users
  941. var strippedZone = String(bolusDate.dropLast())
  942. strippedZone = strippedZone.components(separatedBy: ".")[0]
  943. let dateFormatter = DateFormatter()
  944. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  945. dateFormatter.locale = Locale(identifier: "en_US")
  946. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  947. let dateString = dateFormatter.date(from: strippedZone)
  948. let dateTimeStamp = dateString!.timeIntervalSince1970
  949. do {
  950. let bolus = try currentEntry?["insulin"] as! Double
  951. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  952. lastFoundIndex = sgv.foundIndex
  953. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  954. // Make the dot
  955. let dot = bolusCarbGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  956. bolusData.append(dot)
  957. }
  958. } catch {
  959. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Bolus") }
  960. }
  961. }
  962. if UserDefaultsRepository.graphBolus.value {
  963. updateBolusGraph()
  964. }
  965. }
  966. // NS Carb Web Call
  967. func webLoadNSCarbs(){
  968. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Carbs") }
  969. if !UserDefaultsRepository.downloadCarbs.value { return }
  970. let yesterdayString = dateTimeUtils.nowMinus24HoursTimeInterval()
  971. let urlUser = UserDefaultsRepository.url.value
  972. var searchString = "find[eventType]=Meal%20Bolus&find[created_at][$gte]=" + yesterdayString
  973. var urlDataPath: String = urlUser + "/api/v1/treatments.json?"
  974. if token == "" {
  975. urlDataPath = urlDataPath + searchString
  976. }
  977. else
  978. {
  979. urlDataPath = urlDataPath + "token=" + token + "&" + searchString
  980. }
  981. guard let urlData = URL(string: urlDataPath) else {
  982. return
  983. }
  984. var request = URLRequest(url: urlData)
  985. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  986. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  987. guard error == nil else {
  988. return
  989. }
  990. guard let data = data else {
  991. return
  992. }
  993. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  994. if let json = json {
  995. DispatchQueue.main.async {
  996. self.processNSCarbs(entries: json)
  997. }
  998. } else {
  999. return
  1000. }
  1001. }
  1002. task.resume()
  1003. }
  1004. // NS Carb Bolus Response Processor
  1005. func processNSCarbs(entries: [[String:AnyObject]]) {
  1006. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Carbs") }
  1007. // because it's a small array, we're going to destroy and reload every time.
  1008. carbData.removeAll()
  1009. var lastFoundIndex = 0
  1010. for i in 0..<entries.count {
  1011. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1012. var carbDate: String
  1013. if currentEntry?["timestamp"] != nil {
  1014. carbDate = currentEntry?["timestamp"] as! String
  1015. } else if currentEntry?["created_at"] != nil {
  1016. carbDate = currentEntry?["created_at"] as! String
  1017. } else {
  1018. return
  1019. }
  1020. // Fix for FreeAPS milliseconds in timestamp
  1021. var strippedZone = String(carbDate.dropLast())
  1022. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1023. let dateFormatter = DateFormatter()
  1024. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1025. dateFormatter.locale = Locale(identifier: "en_US")
  1026. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1027. let dateString = dateFormatter.date(from: strippedZone)
  1028. let dateTimeStamp = dateString!.timeIntervalSince1970
  1029. guard let carbs = currentEntry?["carbs"] as? Double else {
  1030. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Carb entry")}
  1031. break
  1032. }
  1033. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1034. lastFoundIndex = sgv.foundIndex
  1035. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1036. // Make the dot
  1037. let dot = bolusCarbGraphStruct(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1038. carbData.append(dot)
  1039. }
  1040. }
  1041. if UserDefaultsRepository.graphCarbs.value {
  1042. updateCarbGraph()
  1043. }
  1044. }
  1045. }