NightScout.swift 50 KB

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