NightScout.swift 78 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741
  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 bolusGraphStruct: Codable {
  28. var value: Double
  29. var date: TimeInterval
  30. var sgv: Int
  31. }
  32. //NS Bolus Data Struct
  33. struct carbGraphStruct: Codable {
  34. var value: Double
  35. var date: TimeInterval
  36. var sgv: Int
  37. var absorptionTime: Int
  38. }
  39. func isStaleData() -> Bool {
  40. if bgData.count > 0 {
  41. let now = dateTimeUtils.getNowTimeIntervalUTC()
  42. let lastReadingTime = bgData.last!.date
  43. let secondsAgo = now - lastReadingTime
  44. if secondsAgo >= 20*60 {
  45. return true
  46. } else {
  47. return false
  48. }
  49. } else {
  50. return false
  51. }
  52. }
  53. // Dex Share Web Call
  54. func webLoadDexShare(onlyPullLastRecord: Bool = false) {
  55. // Dexcom Share only returns 24 hrs of data as of now
  56. // Requesting more just for consistency with NS
  57. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  58. var count = graphHours * 12
  59. if onlyPullLastRecord { count = 1 }
  60. dexShare?.fetchData(count) { (err, result) -> () in
  61. // TODO: add error checking
  62. if(err == nil) {
  63. var data = result!
  64. // If Dex data is old, load from NS instead
  65. let latestDate = data[0].date
  66. let now = dateTimeUtils.getNowTimeIntervalUTC()
  67. if (latestDate + 330) < now {
  68. self.webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  69. print("dex didn't load, triggered NS attempt")
  70. return
  71. }
  72. // Dexcom only returns 24 hrs of data. If we need more, call NS.
  73. if graphHours > 24 && !onlyPullLastRecord {
  74. self.webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord, dexData: data)
  75. } else {
  76. self.ProcessDexBGData(data: data, onlyPullLastRecord: onlyPullLastRecord, sourceName: "Dexcom")
  77. }
  78. } else {
  79. // If we get an error, immediately try to pull NS BG Data
  80. self.webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  81. if globalVariables.dexVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  82. globalVariables.dexVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  83. DispatchQueue.main.async {
  84. //self.sendNotification(title: "Dexcom Share Error", body: "Please double check user name and password, internet connection, and sharing status.")
  85. }
  86. }
  87. }
  88. }
  89. }
  90. // NS BG Data Web call
  91. func webLoadNSBGData(onlyPullLastRecord: Bool = false, dexData: [ShareGlucoseData] = []) {
  92. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: BG") }
  93. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  94. // Set the count= in the url either to pull day(s) of data or only the last record
  95. var points = "1"
  96. if !onlyPullLastRecord {
  97. points = String(graphHours * 12 + 1)
  98. }
  99. // URL processor
  100. var urlBGDataPath: String = UserDefaultsRepository.url.value + "/api/v1/entries/sgv.json?"
  101. if token == "" {
  102. urlBGDataPath = urlBGDataPath + "count=" + points
  103. } else {
  104. urlBGDataPath = urlBGDataPath + "token=" + token + "&count=" + points
  105. }
  106. guard let urlBGData = URL(string: urlBGDataPath) else {
  107. // if we have Dex data, use it
  108. if !dexData.isEmpty {
  109. self.ProcessDexBGData(data: dexData, onlyPullLastRecord: onlyPullLastRecord, sourceName: "Dexcom")
  110. return
  111. }
  112. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  113. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  114. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  115. }
  116. DispatchQueue.main.async {
  117. if self.bgTimer.isValid {
  118. self.bgTimer.invalidate()
  119. }
  120. self.startBGTimer(time: 10)
  121. }
  122. return
  123. }
  124. var request = URLRequest(url: urlBGData)
  125. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  126. // Downloader
  127. let getBGTask = URLSession.shared.dataTask(with: request) { data, response, error in
  128. guard error == nil else {
  129. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  130. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  131. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  132. }
  133. DispatchQueue.main.async {
  134. if self.bgTimer.isValid {
  135. self.bgTimer.invalidate()
  136. }
  137. self.startBGTimer(time: 10)
  138. }
  139. // if we have Dex data, use it
  140. if !dexData.isEmpty {
  141. self.ProcessDexBGData(data: dexData, onlyPullLastRecord: onlyPullLastRecord, sourceName: "Dexcom")
  142. }
  143. return
  144. }
  145. guard let data = data else {
  146. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  147. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  148. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  149. }
  150. DispatchQueue.main.async {
  151. if self.bgTimer.isValid {
  152. self.bgTimer.invalidate()
  153. }
  154. self.startBGTimer(time: 10)
  155. }
  156. return
  157. }
  158. let decoder = JSONDecoder()
  159. let entriesResponse = try? decoder.decode([ShareGlucoseData].self, from: data)
  160. if var nsData = entriesResponse {
  161. DispatchQueue.main.async {
  162. // transform NS data to look like Dex data
  163. for i in 0..<nsData.count {
  164. // convert the NS timestamp to seconds instead of milliseconds
  165. nsData[i].date /= 1000
  166. nsData[i].date.round(FloatingPointRoundingRule.toNearestOrEven)
  167. }
  168. // merge NS and Dex data if needed; use recent Dex data and older NS data
  169. var sourceName = "Nightscout"
  170. if !dexData.isEmpty {
  171. let oldestDexDate = dexData[dexData.count - 1].date
  172. var itemsToRemove = 0
  173. while itemsToRemove < nsData.count && nsData[itemsToRemove].date >= oldestDexDate {
  174. itemsToRemove += 1
  175. }
  176. nsData.removeFirst(itemsToRemove)
  177. nsData = dexData + nsData
  178. sourceName = "Dexcom"
  179. }
  180. // trigger the processor for the data after downloading.
  181. self.ProcessDexBGData(data: nsData, onlyPullLastRecord: onlyPullLastRecord, sourceName: sourceName)
  182. }
  183. } else {
  184. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  185. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  186. //self.sendNotification(title: "Nightscout Failure", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  187. }
  188. DispatchQueue.main.async {
  189. if self.bgTimer.isValid {
  190. self.bgTimer.invalidate()
  191. }
  192. self.startBGTimer(time: 10)
  193. }
  194. return
  195. }
  196. }
  197. getBGTask.resume()
  198. }
  199. // Dexcom BG Data Response processor
  200. func ProcessDexBGData(data: [ShareGlucoseData], onlyPullLastRecord: Bool, sourceName: String){
  201. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: BG") }
  202. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  203. let pullDate = data[data.count - 1].date
  204. let latestDate = data[0].date
  205. let now = dateTimeUtils.getNowTimeIntervalUTC()
  206. // Start the BG timer based on the reading
  207. let secondsAgo = now - latestDate
  208. DispatchQueue.main.async {
  209. // if reading is overdue over: 20:00, re-attempt every 5 minutes
  210. if secondsAgo >= (20 * 60) {
  211. self.startBGTimer(time: (5 * 60))
  212. print("##### started 5 minute bg timer")
  213. // if the reading is overdue: 10:00-19:59, re-attempt every minute
  214. } else if secondsAgo >= (10 * 60) {
  215. self.startBGTimer(time: 60)
  216. print("##### started 1 minute bg timer")
  217. // if the reading is overdue: 7:00-9:59, re-attempt every 30 seconds
  218. } else if secondsAgo >= (7 * 60) {
  219. self.startBGTimer(time: 30)
  220. print("##### started 30 second bg timer")
  221. // if the reading is overdue: 5:00-6:59 re-attempt every 10 seconds
  222. } else if secondsAgo >= (5 * 60) {
  223. self.startBGTimer(time: 10)
  224. print("##### started 10 second bg timer")
  225. // We have a current reading. Set timer to 5:10 from last reading
  226. } else {
  227. self.startBGTimer(time: 300 - secondsAgo + Double(UserDefaultsRepository.bgUpdateDelay.value))
  228. let timerVal = 310 - secondsAgo
  229. print("##### started 5:10 bg timer: \(timerVal)")
  230. }
  231. }
  232. // 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
  233. if !onlyPullLastRecord {
  234. bgData.removeAll()
  235. } else if bgData[bgData.count - 1].date != pullDate {
  236. bgData.removeFirst()
  237. if data.count > 0 && UserDefaultsRepository.speakBG.value {
  238. speakBG(sgv: data[data.count - 1].sgv)
  239. }
  240. } else {
  241. if data.count > 0 {
  242. self.updateBadge(val: data[data.count - 1].sgv)
  243. }
  244. return
  245. }
  246. // loop through the data so we can reverse the order to oldest first for the graph
  247. for i in 0..<data.count{
  248. let dateString = data[data.count - 1 - i].date
  249. if dateString >= dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) {
  250. let reading = ShareGlucoseData(sgv: data[data.count - 1 - i].sgv, date: dateString, direction: data[data.count - 1 - i].direction)
  251. bgData.append(reading)
  252. }
  253. }
  254. viewUpdateNSBG(sourceName: sourceName)
  255. }
  256. // NS BG Data Front end updater
  257. func viewUpdateNSBG (sourceName: String) {
  258. DispatchQueue.main.async {
  259. if UserDefaultsRepository.debugLog.value {
  260. self.writeDebugLog(value: "Display: BG")
  261. self.writeDebugLog(value: "Num BG: " + self.bgData.count.description)
  262. }
  263. let entries = self.bgData
  264. if entries.count < 1 { return }
  265. self.updateBGGraph()
  266. self.updateStats()
  267. let latestEntryi = entries.count - 1
  268. let latestBG = entries[latestEntryi].sgv
  269. let priorBG = entries[latestEntryi - 1].sgv
  270. let deltaBG = latestBG - priorBG as Int
  271. let lastBGTime = entries[latestEntryi].date
  272. let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-lastBGTime) / 60
  273. var userUnit = " mg/dL"
  274. if self.mmol {
  275. userUnit = " mmol/L"
  276. }
  277. self.serverText.text = sourceName
  278. var snoozerBG = ""
  279. var snoozerDirection = ""
  280. var snoozerDelta = ""
  281. self.BGText.text = bgUnits.toDisplayUnits(String(latestBG))
  282. snoozerBG = bgUnits.toDisplayUnits(String(latestBG))
  283. self.setBGTextColor()
  284. if let directionBG = entries[latestEntryi].direction {
  285. self.DirectionText.text = self.bgDirectionGraphic(directionBG)
  286. snoozerDirection = self.bgDirectionGraphic(directionBG)
  287. self.latestDirectionString = self.bgDirectionGraphic(directionBG)
  288. }
  289. else
  290. {
  291. self.DirectionText.text = ""
  292. snoozerDirection = ""
  293. self.latestDirectionString = ""
  294. }
  295. if deltaBG < 0 {
  296. self.DeltaText.text = bgUnits.toDisplayUnits(String(deltaBG))
  297. snoozerDelta = bgUnits.toDisplayUnits(String(deltaBG))
  298. self.latestDeltaString = String(deltaBG)
  299. }
  300. else
  301. {
  302. self.DeltaText.text = "+" + bgUnits.toDisplayUnits(String(deltaBG))
  303. snoozerDelta = "+" + bgUnits.toDisplayUnits(String(deltaBG))
  304. self.latestDeltaString = "+" + String(deltaBG)
  305. }
  306. self.updateBadge(val: latestBG)
  307. // Snoozer Display
  308. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  309. snoozer.BGLabel.text = snoozerBG
  310. snoozer.DirectionLabel.text = snoozerDirection
  311. snoozer.DeltaLabel.text = snoozerDelta
  312. }
  313. }
  314. // NS Device Status Web Call
  315. func webLoadNSDeviceStatus() {
  316. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: device status") }
  317. let urlUser = UserDefaultsRepository.url.value
  318. // NS Api is not working to find by greater than date
  319. var urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=288"
  320. if token != "" {
  321. urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=288&token=" + token
  322. }
  323. let escapedAddress = urlStringDeviceStatus.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  324. guard let urlDeviceStatus = URL(string: escapedAddress!) else {
  325. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  326. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  327. //self.sendNotification(title: "Nightscout Failure", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  328. }
  329. DispatchQueue.main.async {
  330. if self.deviceStatusTimer.isValid {
  331. self.deviceStatusTimer.invalidate()
  332. }
  333. self.startDeviceStatusTimer(time: 10)
  334. }
  335. return
  336. }
  337. var requestDeviceStatus = URLRequest(url: urlDeviceStatus)
  338. requestDeviceStatus.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  339. let deviceStatusTask = URLSession.shared.dataTask(with: requestDeviceStatus) { data, response, error in
  340. guard error == nil else {
  341. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  342. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  343. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  344. }
  345. DispatchQueue.main.async {
  346. if self.deviceStatusTimer.isValid {
  347. self.deviceStatusTimer.invalidate()
  348. }
  349. self.startDeviceStatusTimer(time: 10)
  350. }
  351. return
  352. }
  353. guard let data = data else {
  354. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  355. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  356. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  357. }
  358. DispatchQueue.main.async {
  359. if self.deviceStatusTimer.isValid {
  360. self.deviceStatusTimer.invalidate()
  361. }
  362. self.startDeviceStatusTimer(time: 10)
  363. }
  364. return
  365. }
  366. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  367. if let json = json {
  368. DispatchQueue.main.async {
  369. self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
  370. }
  371. } else {
  372. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  373. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  374. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  375. }
  376. DispatchQueue.main.async {
  377. if self.deviceStatusTimer.isValid {
  378. self.deviceStatusTimer.invalidate()
  379. }
  380. self.startDeviceStatusTimer(time: 10)
  381. }
  382. return
  383. }
  384. }
  385. deviceStatusTask.resume()
  386. }
  387. // NS Device Status Response Processor
  388. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  389. self.clearLastInfoData(index: 0)
  390. self.clearLastInfoData(index: 1)
  391. self.clearLastInfoData(index: 3)
  392. self.clearLastInfoData(index: 4)
  393. self.clearLastInfoData(index: 5)
  394. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
  395. if jsonDeviceStatus.count == 0 {
  396. return
  397. }
  398. //Process the current data first
  399. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  400. //pump and uploader
  401. let formatter = ISO8601DateFormatter()
  402. formatter.formatOptions = [.withFullDate,
  403. .withTime,
  404. .withDashSeparatorInDate,
  405. .withColonSeparatorInTime]
  406. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  407. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  408. if let reservoirData = lastPumpRecord["reservoir"] as? Double {
  409. latestPumpVolume = reservoirData
  410. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  411. } else {
  412. latestPumpVolume = 50.0
  413. tableData[5].value = "50+U"
  414. }
  415. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  416. let upbat = uploader["battery"] as! Double
  417. tableData[4].value = String(format:"%.0f", upbat) + "%"
  418. }
  419. }
  420. }
  421. // Loop
  422. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  423. //print("Loop: \(lastLoopRecord)")
  424. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  425. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  426. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  427. if let failure = lastLoopRecord["failureReason"] {
  428. LoopStatusLabel.text = "X"
  429. latestLoopStatusString = "X"
  430. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  431. } else {
  432. var wasEnacted = false
  433. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  434. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  435. wasEnacted = true
  436. if let lastTempBasal = enacted["rate"] as? Double {
  437. }
  438. }
  439. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  440. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  441. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  442. }
  443. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  444. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  445. latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
  446. }
  447. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  448. let prediction = predictdata["values"] as! [Int]
  449. PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
  450. PredictionLabel.textColor = UIColor.systemPurple
  451. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  452. predictionData.removeAll()
  453. var predictionTime = lastLoopTime
  454. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  455. var i = 0
  456. while i <= toLoad {
  457. if i < prediction.count {
  458. let prediction = ShareGlucoseData(sgv: prediction[i], date: predictionTime, direction: "flat")
  459. predictionData.append(prediction)
  460. predictionTime += 300
  461. }
  462. i += 1
  463. }
  464. let predMin = prediction.min()
  465. let predMax = prediction.max()
  466. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  467. updatePredictionGraph()
  468. }
  469. }
  470. if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
  471. tableData[8].value = String(format:"%.2fU", recBolus)
  472. }
  473. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  474. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  475. var lastBGTime = lastLoopTime
  476. if bgData.count > 0 {
  477. lastBGTime = bgData[bgData.count - 1].date
  478. }
  479. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  480. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  481. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  482. if tempBasalTime > lastBGTime && !wasEnacted {
  483. LoopStatusLabel.text = "⏀"
  484. latestLoopStatusString = "⏀"
  485. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  486. } else {
  487. LoopStatusLabel.text = "↻"
  488. latestLoopStatusString = "↻"
  489. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  490. }
  491. }
  492. } else {
  493. LoopStatusLabel.text = "↻"
  494. latestLoopStatusString = "↻"
  495. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  496. }
  497. }
  498. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 {
  499. LoopStatusLabel.text = "⚠"
  500. latestLoopStatusString = "⚠"
  501. }
  502. latestLoopTime = lastLoopTime
  503. } // end lastLoopTime
  504. } // end lastLoop Record
  505. var oText = "" as String
  506. currentOverride = 1.0
  507. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  508. if let lastOverrideTime = formatter.date(from: (lastOverride["timestamp"] as! String))?.timeIntervalSince1970 {
  509. }
  510. if lastOverride["active"] as! Bool {
  511. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  512. if let multiplier = lastOverride["multiplier"] as? Double {
  513. currentOverride = multiplier
  514. oText += String(format: "%.0f%%", (multiplier * 100))
  515. }
  516. else
  517. {
  518. oText += "100%"
  519. }
  520. oText += " ("
  521. let minValue = lastCorrection["minValue"] as! Double
  522. let maxValue = lastCorrection["maxValue"] as! Double
  523. oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
  524. tableData[3].value = oText
  525. }
  526. }
  527. infoTable.reloadData()
  528. // Start the timer based on the timestamp
  529. let now = dateTimeUtils.getNowTimeIntervalUTC()
  530. let secondsAgo = now - latestLoopTime
  531. DispatchQueue.main.async {
  532. // if Loop is overdue over: 20:00, re-attempt every 5 minutes
  533. if secondsAgo >= (20 * 60) {
  534. self.startDeviceStatusTimer(time: (5 * 60))
  535. print("started 5 minute device status timer")
  536. // if the Loop is overdue: 10:00-19:59, re-attempt every minute
  537. } else if secondsAgo >= (10 * 60) {
  538. self.startDeviceStatusTimer(time: 60)
  539. print("started 1 minute device status timer")
  540. // if the Loop is overdue: 7:00-9:59, re-attempt every 30 seconds
  541. } else if secondsAgo >= (7 * 60) {
  542. self.startDeviceStatusTimer(time: 30)
  543. print("started 30 second device status timer")
  544. // if the Loop is overdue: 5:00-6:59 re-attempt every 10 seconds
  545. } else if secondsAgo >= (5 * 60) {
  546. self.startDeviceStatusTimer(time: 10)
  547. print("started 10 second device status timer")
  548. // We have a current Loop. Set timer to 5:10 from last reading
  549. } else {
  550. self.startDeviceStatusTimer(time: 310 - secondsAgo)
  551. let timerVal = 310 - secondsAgo
  552. print("started 5:10 device status timer: \(timerVal)")
  553. }
  554. }
  555. }
  556. // NS Cage Web Call
  557. func webLoadNSCage() {
  558. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: CAGE") }
  559. let urlUser = UserDefaultsRepository.url.value
  560. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Site%20Change&count=1"
  561. if token != "" {
  562. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Site%20Change&count=1"
  563. }
  564. guard let urlData = URL(string: urlString) else {
  565. return
  566. }
  567. var request = URLRequest(url: urlData)
  568. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  569. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  570. guard error == nil else {
  571. return
  572. }
  573. guard let data = data else {
  574. return
  575. }
  576. let decoder = JSONDecoder()
  577. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  578. if let entriesResponse = entriesResponse {
  579. DispatchQueue.main.async {
  580. self.updateCage(data: entriesResponse)
  581. }
  582. } else {
  583. return
  584. }
  585. }
  586. task.resume()
  587. }
  588. // NS Cage Response Processor
  589. func updateCage(data: [cageData]) {
  590. self.clearLastInfoData(index: 7)
  591. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: CAGE") }
  592. if data.count == 0 {
  593. return
  594. }
  595. let lastCageString = data[0].created_at
  596. let formatter = ISO8601DateFormatter()
  597. formatter.formatOptions = [.withFullDate,
  598. .withTime,
  599. .withDashSeparatorInDate,
  600. .withColonSeparatorInTime]
  601. UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval
  602. if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 {
  603. let now = dateTimeUtils.getNowTimeIntervalUTC()
  604. let secondsAgo = now - cageTime
  605. //let days = 24 * 60 * 60
  606. let formatter = DateComponentsFormatter()
  607. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  608. formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string
  609. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  610. let formattedDuration = formatter.string(from: secondsAgo)
  611. tableData[7].value = formattedDuration ?? ""
  612. }
  613. infoTable.reloadData()
  614. }
  615. // NS Sage Web Call
  616. func webLoadNSSage() {
  617. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: SAGE") }
  618. let lastDateString = dateTimeUtils.nowMinus10DaysTimeInterval()
  619. let urlUser = UserDefaultsRepository.url.value
  620. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  621. if token != "" {
  622. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  623. }
  624. guard let urlData = URL(string: urlString) else {
  625. return
  626. }
  627. var request = URLRequest(url: urlData)
  628. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  629. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  630. guard error == nil else {
  631. return
  632. }
  633. guard let data = data else {
  634. return
  635. }
  636. let decoder = JSONDecoder()
  637. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  638. if let entriesResponse = entriesResponse {
  639. DispatchQueue.main.async {
  640. self.updateSage(data: entriesResponse)
  641. }
  642. } else {
  643. return
  644. }
  645. }
  646. task.resume()
  647. }
  648. // NS Sage Response Processor
  649. func updateSage(data: [cageData]) {
  650. self.clearLastInfoData(index: 6)
  651. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process/Display: SAGE") }
  652. if data.count == 0 {
  653. return
  654. }
  655. var lastSageString = data[0].created_at
  656. let formatter = ISO8601DateFormatter()
  657. formatter.formatOptions = [.withFullDate,
  658. .withTime,
  659. .withDashSeparatorInDate,
  660. .withColonSeparatorInTime]
  661. UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: (lastSageString))?.timeIntervalSince1970 as! TimeInterval
  662. if UserDefaultsRepository.alertAutoSnoozeCGMStart.value && (dateTimeUtils.getNowTimeIntervalUTC() - UserDefaultsRepository.alertSageInsertTime.value < 7200){
  663. let snoozeTime = Date(timeIntervalSince1970: UserDefaultsRepository.alertSageInsertTime.value + 7200)
  664. UserDefaultsRepository.alertSnoozeAllTime.value = snoozeTime
  665. UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = true
  666. guard let alarms = self.tabBarController!.viewControllers?[1] as? AlarmViewController else { return }
  667. alarms.reloadIsSnoozed(key: "alertSnoozeAllIsSnoozed", value: true)
  668. alarms.reloadSnoozeTime(key: "alertSnoozeAllTime", setNil: false, value: snoozeTime)
  669. }
  670. if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 {
  671. let now = dateTimeUtils.getNowTimeIntervalUTC()
  672. let secondsAgo = now - sageTime
  673. let days = 24 * 60 * 60
  674. let formatter = DateComponentsFormatter()
  675. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  676. formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
  677. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  678. let formattedDuration = formatter.string(from: secondsAgo)
  679. tableData[6].value = formattedDuration ?? ""
  680. }
  681. infoTable.reloadData()
  682. }
  683. // NS Profile Web Call
  684. func webLoadNSProfile() {
  685. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: profile") }
  686. let urlUser = UserDefaultsRepository.url.value
  687. var urlString = urlUser + "/api/v1/profile/current.json"
  688. if token != "" {
  689. urlString = urlUser + "/api/v1/profile/current.json?token=" + token
  690. }
  691. let escapedAddress = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  692. guard let url = URL(string: escapedAddress!) else {
  693. return
  694. }
  695. var request = URLRequest(url: url)
  696. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  697. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  698. guard error == nil else {
  699. return
  700. }
  701. guard let data = data else {
  702. return
  703. }
  704. let json = try? JSONSerialization.jsonObject(with: data) as! Dictionary<String, Any>
  705. if let json = json {
  706. DispatchQueue.main.async {
  707. self.updateProfile(jsonDeviceStatus: json)
  708. }
  709. } else {
  710. return
  711. }
  712. }
  713. task.resume()
  714. }
  715. // NS Profile Response Processor
  716. func updateProfile(jsonDeviceStatus: Dictionary<String, Any>) {
  717. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: profile") }
  718. if jsonDeviceStatus.count == 0 {
  719. return
  720. }
  721. if jsonDeviceStatus[keyPath: "message"] != nil { return }
  722. let basal = try jsonDeviceStatus[keyPath: "store.Default.basal"] as! NSArray
  723. basalProfile.removeAll()
  724. for i in 0..<basal.count {
  725. let dict = basal[i] as! Dictionary<String, Any>
  726. do {
  727. let thisValue = try dict[keyPath: "value"] as! Double
  728. let thisTime = dict[keyPath: "time"] as! String
  729. let thisTimeAsSeconds = dict[keyPath: "timeAsSeconds"] as! Double
  730. let entry = basalProfileStruct(value: thisValue, time: thisTime, timeAsSeconds: thisTimeAsSeconds)
  731. basalProfile.append(entry)
  732. } catch {
  733. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: profile wrapped in quotes") }
  734. }
  735. }
  736. // Don't process the basal or draw the graph until after the BG has been fully processeed and drawn
  737. if firstGraphLoad { return }
  738. var basalSegments: [DataStructs.basalProfileSegment] = []
  739. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  740. // Build scheduled basal segments from right to left by
  741. // moving pointers to the current midnight and current basal
  742. var midnight = dateTimeUtils.getTimeIntervalMidnightToday()
  743. var basalProfileIndex = basalProfile.count - 1
  744. var start = midnight + basalProfile[basalProfileIndex].timeAsSeconds
  745. var end = dateTimeUtils.getNowTimeIntervalUTC()
  746. // Move back until we're in the graph range
  747. while start > end {
  748. basalProfileIndex -= 1
  749. start = midnight + basalProfile[basalProfileIndex].timeAsSeconds
  750. }
  751. // Add records while they're still within the graph
  752. let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours)
  753. while end >= graphStart {
  754. let entry = DataStructs.basalProfileSegment(
  755. basalRate: basalProfile[basalProfileIndex].value, startDate: start, endDate: end)
  756. basalSegments.append(entry)
  757. basalProfileIndex -= 1
  758. if basalProfileIndex < 0 {
  759. basalProfileIndex = basalProfile.count - 1
  760. midnight = midnight.advanced(by: -24*60*60)
  761. }
  762. end = start - 1
  763. start = midnight + basalProfile[basalProfileIndex].timeAsSeconds
  764. }
  765. // reverse the result to get chronological order
  766. basalSegments.reverse()
  767. var firstPass = true
  768. // Runs the scheduled basal to the end of the prediction line
  769. var predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * UserDefaultsRepository.predictionToLoad.value)
  770. basalScheduleData.removeAll()
  771. for i in 0..<basalSegments.count {
  772. let timeStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours)
  773. // This processed everything after the first one.
  774. if firstPass == false
  775. && basalSegments[i].startDate <= predictionEndTime {
  776. let startDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: basalSegments[i].startDate)
  777. basalScheduleData.append(startDot)
  778. var endDate = basalSegments[i].endDate
  779. // if it's the last one needed, set it to end at the prediction end time
  780. if endDate > predictionEndTime || i == basalSegments.count - 1 {
  781. endDate = Double(predictionEndTime)
  782. }
  783. let endDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: endDate)
  784. basalScheduleData.append(endDot)
  785. }
  786. // we need to manually set the first one
  787. // Check that this is the first one and there are no existing entries
  788. if firstPass == true {
  789. // check that the timestamp is > the current entry and < the next entry
  790. if timeStart >= basalSegments[i].startDate && timeStart < basalSegments[i].endDate {
  791. // Set the start time to match the BG start
  792. let startDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: Double(timeStart + (60 * 5)))
  793. basalScheduleData.append(startDot)
  794. // set the enddot where the next one will start
  795. var endDate = basalSegments[i].endDate
  796. let endDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: endDate)
  797. basalScheduleData.append(endDot)
  798. firstPass = false
  799. }
  800. }
  801. }
  802. if UserDefaultsRepository.graphBasal.value {
  803. updateBasalScheduledGraph()
  804. }
  805. }
  806. // NS Treatments Web Call
  807. // Downloads Basal, Bolus, Carbs, BG Check, Notes, Overrides
  808. func WebLoadNSTreatments() {
  809. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Treatments") }
  810. if !UserDefaultsRepository.downloadTreatments.value { return }
  811. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  812. let startTimeString = dateTimeUtils.nowMinusNHoursTimeInterval(N: graphHours)
  813. var urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?find[created_at][$gte]=" + startTimeString
  814. if token != "" {
  815. urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?token=" + token + "&find[created_at][$gte]=" + startTimeString
  816. }
  817. guard let urlData = URL(string: urlString) else {
  818. return
  819. }
  820. var request = URLRequest(url: urlData)
  821. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  822. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  823. guard error == nil else {
  824. return
  825. }
  826. guard let data = data else {
  827. return
  828. }
  829. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  830. if let json = json {
  831. DispatchQueue.main.async {
  832. self.updateTreatments(entries: json)
  833. }
  834. } else {
  835. return
  836. }
  837. }
  838. task.resume()
  839. }
  840. // Process and split out treatments to individual tasks
  841. func updateTreatments(entries: [[String:AnyObject]]) {
  842. var tempBasal: [[String:AnyObject]] = []
  843. var bolus: [[String:AnyObject]] = []
  844. var carbs: [[String:AnyObject]] = []
  845. var temporaryOverride: [[String:AnyObject]] = []
  846. var note: [[String:AnyObject]] = []
  847. var bgCheck: [[String:AnyObject]] = []
  848. var suspendPump: [[String:AnyObject]] = []
  849. var resumePump: [[String:AnyObject]] = []
  850. var pumpSiteChange: [[String:AnyObject]] = []
  851. var cgmSensorStart: [[String:AnyObject]] = []
  852. for i in 0..<entries.count {
  853. let entry = entries[i] as [String : AnyObject]?
  854. switch entry?["eventType"] as! String {
  855. case "Temp Basal":
  856. tempBasal.append(entry!)
  857. case "Correction Bolus":
  858. bolus.append(entry!)
  859. case "Meal Bolus":
  860. carbs.append(entry!)
  861. case "Temporary Override":
  862. temporaryOverride.append(entry!)
  863. case "Note":
  864. note.append(entry!)
  865. print("Note: \(String(describing: entry))")
  866. case "BG Check":
  867. bgCheck.append(entry!)
  868. case "Suspend Pump":
  869. suspendPump.append(entry!)
  870. case "Resume Pump":
  871. resumePump.append(entry!)
  872. case "Pump Site Change":
  873. pumpSiteChange.append(entry!)
  874. case "Sensor Start":
  875. cgmSensorStart.append(entry!)
  876. default:
  877. print("No Match: \(String(describing: entry))")
  878. }
  879. }
  880. // end of for loop
  881. if tempBasal.count > 0 {
  882. processNSBasals(entries: tempBasal)
  883. } else {
  884. if basalData.count < 0 {
  885. clearOldTempBasal()
  886. }
  887. }
  888. if bolus.count > 0 {
  889. processNSBolus(entries: bolus)
  890. } else {
  891. if bolusData.count > 0 {
  892. clearOldBolus()
  893. }
  894. }
  895. if carbs.count > 0 {
  896. processNSCarbs(entries: carbs)
  897. } else {
  898. if carbData.count > 0 {
  899. clearOldCarb()
  900. }
  901. }
  902. if bgCheck.count > 0 {
  903. processNSBGCheck(entries: bgCheck)
  904. } else {
  905. if bgCheckData.count > 0 {
  906. clearOldBGCheck()
  907. }
  908. }
  909. if temporaryOverride.count > 0 {
  910. processNSOverrides(entries: temporaryOverride)
  911. } else {
  912. if overrideGraphData.count > 0 {
  913. clearOldOverride()
  914. }
  915. }
  916. if suspendPump.count > 0 {
  917. processSuspendPump(entries: suspendPump)
  918. } else {
  919. if suspendGraphData.count > 0 {
  920. clearOldSuspend()
  921. }
  922. }
  923. if resumePump.count > 0 {
  924. processResumePump(entries: resumePump)
  925. } else {
  926. if resumeGraphData.count > 0 {
  927. clearOldResume()
  928. }
  929. }
  930. if cgmSensorStart.count > 0 {
  931. processSensorStart(entries: cgmSensorStart)
  932. } else {
  933. if sensorStartGraphData.count > 0 {
  934. clearOldSensor()
  935. }
  936. }
  937. if note.count > 0 {
  938. processNotes(entries: note)
  939. } else {
  940. if noteGraphData.count > 0 {
  941. clearOldNotes()
  942. }
  943. }
  944. }
  945. func clearOldTempBasal()
  946. {
  947. basalData.removeAll()
  948. updateBasalGraph()
  949. }
  950. func clearOldBolus()
  951. {
  952. bolusData.removeAll()
  953. updateBolusGraph()
  954. }
  955. func clearOldCarb()
  956. {
  957. carbData.removeAll()
  958. updateCarbGraph()
  959. }
  960. func clearOldBGCheck()
  961. {
  962. bgCheckData.removeAll()
  963. updateBGCheckGraph()
  964. }
  965. func clearOldOverride()
  966. {
  967. overrideGraphData.removeAll()
  968. updateOverrideGraph()
  969. }
  970. func clearOldSuspend()
  971. {
  972. suspendGraphData.removeAll()
  973. updateSuspendGraph()
  974. }
  975. func clearOldResume()
  976. {
  977. resumeGraphData.removeAll()
  978. updateResumeGraph()
  979. }
  980. func clearOldSensor()
  981. {
  982. sensorStartGraphData.removeAll()
  983. updateSensorStart()
  984. }
  985. func clearOldNotes()
  986. {
  987. noteGraphData.removeAll()
  988. updateNotes()
  989. }
  990. // NS Temp Basal Response Processor
  991. func processNSBasals(entries: [[String:AnyObject]]) {
  992. self.clearLastInfoData(index: 2)
  993. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") }
  994. // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
  995. basalData.removeAll()
  996. var lastEndDot = 0.0
  997. var tempArray = entries
  998. tempArray.reverse()
  999. for i in 0..<tempArray.count {
  1000. let currentEntry = tempArray[i] as [String : AnyObject]?
  1001. var basalDate: String
  1002. if currentEntry?["timestamp"] != nil {
  1003. basalDate = currentEntry?["timestamp"] as! String
  1004. } else if currentEntry?["created_at"] != nil {
  1005. basalDate = currentEntry?["created_at"] as! String
  1006. } else {
  1007. continue
  1008. }
  1009. var strippedZone = String(basalDate.dropLast())
  1010. strippedZone = strippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
  1011. let dateFormatter = DateFormatter()
  1012. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1013. dateFormatter.locale = Locale(identifier: "en_US")
  1014. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1015. guard let dateString = dateFormatter.date(from: strippedZone) else { continue }
  1016. let dateTimeStamp = dateString.timeIntervalSince1970
  1017. guard let basalRate = currentEntry?["absolute"] as? Double else {
  1018. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Basal entry")}
  1019. continue
  1020. }
  1021. let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
  1022. // Setting end dots
  1023. var duration = 0.0
  1024. do {
  1025. duration = try currentEntry?["duration"] as! Double
  1026. } catch {
  1027. print("No Duration Found")
  1028. }
  1029. // 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
  1030. if i > 0 {
  1031. let priorEntry = tempArray[i - 1] as [String : AnyObject]?
  1032. var priorBasalDate: String
  1033. if priorEntry?["timestamp"] != nil {
  1034. priorBasalDate = priorEntry?["timestamp"] as! String
  1035. } else if currentEntry?["created_at"] != nil {
  1036. priorBasalDate = priorEntry?["created_at"] as! String
  1037. } else {
  1038. continue
  1039. }
  1040. var priorStrippedZone = String(priorBasalDate.dropLast())
  1041. priorStrippedZone = priorStrippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
  1042. let priorDateFormatter = DateFormatter()
  1043. priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1044. priorDateFormatter.locale = Locale(identifier: "en_US")
  1045. priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1046. guard let priorDateString = dateFormatter.date(from: priorStrippedZone) else { continue }
  1047. let priorDateTimeStamp = priorDateString.timeIntervalSince1970
  1048. let priorDuration = priorEntry?["duration"] as! Double
  1049. // 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
  1050. if Double( dateTimeStamp - priorDateTimeStamp ) > Double( (priorDuration * 60) + 15 ) {
  1051. var scheduled = 0.0
  1052. var midGap = false
  1053. var midGapTime: TimeInterval = 0
  1054. var midGapValue: Double = 0
  1055. // cycle through basal profiles.
  1056. // TODO figure out how to deal with profile changes that happen mid-gap
  1057. for b in 0..<self.basalScheduleData.count {
  1058. if (priorDateTimeStamp + (priorDuration * 60)) >= basalScheduleData[b].date {
  1059. scheduled = basalScheduleData[b].basalRate
  1060. // deal with mid-gap scheduled basal change
  1061. // don't do it on the last scheudled basal entry
  1062. if b < self.basalScheduleData.count - 1 {
  1063. if dateTimeStamp > self.basalScheduleData[b + 1].date {
  1064. // midGap = true
  1065. // TODO: finish this to handle mid-gap items without crashing from overlapping entries
  1066. midGapTime = self.basalScheduleData[b + 1].date
  1067. midGapValue = self.basalScheduleData[b + 1].basalRate
  1068. }
  1069. }
  1070. }
  1071. }
  1072. // Make the starting dot at the last ending dot
  1073. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
  1074. basalData.append(startDot)
  1075. if midGap {
  1076. // Make the ending dot at the new scheduled basal
  1077. let endDot1 = basalGraphStruct(basalRate: scheduled, date: Double(midGapTime))
  1078. basalData.append(endDot1)
  1079. // Make the starting dot at the scheduled Time
  1080. let startDot2 = basalGraphStruct(basalRate: midGapValue, date: Double(midGapTime))
  1081. basalData.append(startDot2)
  1082. // Make the ending dot at the new basal value
  1083. let endDot2 = basalGraphStruct(basalRate: midGapValue, date: Double(dateTimeStamp))
  1084. basalData.append(endDot2)
  1085. } else {
  1086. // Make the ending dot at the new starting dot
  1087. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
  1088. basalData.append(endDot)
  1089. }
  1090. }
  1091. }
  1092. // Make the starting dot
  1093. let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))
  1094. basalData.append(startDot)
  1095. // Make the ending dot
  1096. // If it's the last one and has no duration, extend it for 30 minutes past the start. Otherwise set ending at duration
  1097. // duration is already set to 0 if there is no duration set on it.
  1098. //if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
  1099. if i == tempArray.count - 1 && duration == 0.0 {
  1100. lastEndDot = dateTimeStamp + (30 * 60)
  1101. latestBasal = String(format:"%.2f", basalRate)
  1102. } else {
  1103. lastEndDot = dateTimeStamp + (duration * 60)
  1104. latestBasal = String(format:"%.2f", basalRate)
  1105. }
  1106. // Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy
  1107. if i < tempArray.count - 1 {
  1108. let nextEntry = tempArray[i + 1] as [String : AnyObject]?
  1109. var nextBasalDate: String
  1110. if nextEntry?["timestamp"] != nil {
  1111. nextBasalDate = nextEntry?["timestamp"] as! String
  1112. } else if currentEntry?["created_at"] != nil {
  1113. nextBasalDate = nextEntry?["created_at"] as! String
  1114. } else {
  1115. continue
  1116. }
  1117. var nextStrippedZone = String(nextBasalDate.dropLast())
  1118. nextStrippedZone = nextStrippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
  1119. let nextDateFormatter = DateFormatter()
  1120. nextDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1121. nextDateFormatter.locale = Locale(identifier: "en_US")
  1122. nextDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1123. guard let nextDateString = dateFormatter.date(from: nextStrippedZone) else { continue }
  1124. let nextDateTimeStamp = nextDateString.timeIntervalSince1970
  1125. if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) {
  1126. lastEndDot = nextDateTimeStamp
  1127. }
  1128. }
  1129. let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))
  1130. basalData.append(endDot)
  1131. }
  1132. // If last basal was prior to right now, we need to create one last scheduled entry
  1133. if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() {
  1134. var scheduled = 0.0
  1135. // cycle through basal profiles.
  1136. // TODO figure out how to deal with profile changes that happen mid-gap
  1137. for b in 0..<self.basalProfile.count {
  1138. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  1139. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  1140. // check the prior temp ending to the profile seconds from midnight
  1141. print("yesterday " + String(scheduleTimeYesterday))
  1142. print("today " + String(scheduleTimeToday))
  1143. if lastEndDot >= scheduleTimeToday {
  1144. scheduled = basalProfile[b].value
  1145. }
  1146. }
  1147. latestBasal = String(format:"%.2f", scheduled)
  1148. // Make the starting dot at the last ending dot
  1149. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
  1150. basalData.append(startDot)
  1151. // Make the ending dot 10 minutes after now
  1152. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970 + (60 * 10)))
  1153. basalData.append(endDot)
  1154. }
  1155. tableData[2].value = latestBasal
  1156. infoTable.reloadData()
  1157. if UserDefaultsRepository.graphBasal.value {
  1158. updateBasalGraph()
  1159. }
  1160. infoTable.reloadData()
  1161. }
  1162. // NS Meal Bolus Response Processor
  1163. func processNSBolus(entries: [[String:AnyObject]]) {
  1164. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Bolus") }
  1165. // because it's a small array, we're going to destroy and reload every time.
  1166. bolusData.removeAll()
  1167. var lastFoundIndex = 0
  1168. for i in 0..<entries.count {
  1169. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1170. var bolusDate: String
  1171. if currentEntry?["timestamp"] != nil {
  1172. bolusDate = currentEntry?["timestamp"] as! String
  1173. } else if currentEntry?["created_at"] != nil {
  1174. bolusDate = currentEntry?["created_at"] as! String
  1175. } else {
  1176. continue
  1177. }
  1178. // fix to remove millisecond (after period in timestamp) for FreeAPS users
  1179. var strippedZone = String(bolusDate.dropLast())
  1180. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1181. let dateFormatter = DateFormatter()
  1182. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1183. dateFormatter.locale = Locale(identifier: "en_US")
  1184. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1185. guard let dateString = dateFormatter.date(from: strippedZone) else { continue }
  1186. let dateTimeStamp = dateString.timeIntervalSince1970
  1187. guard let bolus = currentEntry?["insulin"] as? Double else { continue }
  1188. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1189. lastFoundIndex = sgv.foundIndex
  1190. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1191. // Make the dot
  1192. let dot = bolusGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv + 20))
  1193. bolusData.append(dot)
  1194. }
  1195. }
  1196. if UserDefaultsRepository.graphBolus.value {
  1197. updateBolusGraph()
  1198. }
  1199. }
  1200. // NS Carb Bolus Response Processor
  1201. func processNSCarbs(entries: [[String:AnyObject]]) {
  1202. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Carbs") }
  1203. // because it's a small array, we're going to destroy and reload every time.
  1204. carbData.removeAll()
  1205. var lastFoundIndex = 0
  1206. var lastFoundBolus = 0
  1207. for i in 0..<entries.count {
  1208. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1209. var carbDate: String
  1210. if currentEntry?["timestamp"] != nil {
  1211. carbDate = currentEntry?["timestamp"] as! String
  1212. } else if currentEntry?["created_at"] != nil {
  1213. carbDate = currentEntry?["created_at"] as! String
  1214. } else {
  1215. continue
  1216. }
  1217. let absorptionTime = currentEntry?["absorptionTime"] as? Int ?? 0
  1218. // Fix for FreeAPS milliseconds in timestamp
  1219. var strippedZone = String(carbDate.dropLast())
  1220. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1221. let dateFormatter = DateFormatter()
  1222. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1223. dateFormatter.locale = Locale(identifier: "en_US")
  1224. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1225. guard let dateString = dateFormatter.date(from: strippedZone) else { continue }
  1226. var dateTimeStamp = dateString.timeIntervalSince1970
  1227. guard let carbs = currentEntry?["carbs"] as? Double else {
  1228. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Carb entry")}
  1229. continue
  1230. }
  1231. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1232. lastFoundIndex = sgv.foundIndex
  1233. var offset = -50
  1234. if sgv.sgv < Double(topBG - 100) {
  1235. let bolusTime = findNearestBolusbyTime(timeWithin: 300, needle: dateTimeStamp, haystack: bolusData, startingIndex: lastFoundBolus)
  1236. lastFoundBolus = bolusTime.foundIndex
  1237. if bolusTime.offset {
  1238. offset = 70
  1239. } else {
  1240. offset = 20
  1241. }
  1242. }
  1243. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1244. // Make the dot
  1245. let dot = carbGraphStruct(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv + Double(offset)), absorptionTime: absorptionTime)
  1246. carbData.append(dot)
  1247. }
  1248. }
  1249. if UserDefaultsRepository.graphCarbs.value {
  1250. updateCarbGraph()
  1251. }
  1252. }
  1253. // NS Suspend Pump Response Processor
  1254. func processSuspendPump(entries: [[String:AnyObject]]) {
  1255. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Suspend Pump") }
  1256. // because it's a small array, we're going to destroy and reload every time.
  1257. suspendGraphData.removeAll()
  1258. var lastFoundIndex = 0
  1259. for i in 0..<entries.count {
  1260. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1261. var date: String
  1262. if currentEntry?["timestamp"] != nil {
  1263. date = currentEntry?["timestamp"] as! String
  1264. } else if currentEntry?["created_at"] != nil {
  1265. date = currentEntry?["created_at"] as! String
  1266. } else {
  1267. return
  1268. }
  1269. // Fix for FreeAPS milliseconds in timestamp
  1270. var strippedZone = String(date.dropLast())
  1271. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1272. let dateFormatter = DateFormatter()
  1273. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1274. dateFormatter.locale = Locale(identifier: "en_US")
  1275. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1276. let dateString = dateFormatter.date(from: strippedZone)
  1277. let dateTimeStamp = dateString!.timeIntervalSince1970
  1278. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1279. lastFoundIndex = sgv.foundIndex
  1280. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1281. // Make the dot
  1282. let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1283. suspendGraphData.append(dot)
  1284. }
  1285. }
  1286. if UserDefaultsRepository.graphOtherTreatments.value {
  1287. updateSuspendGraph()
  1288. }
  1289. }
  1290. // NS Resume Pump Response Processor
  1291. func processResumePump(entries: [[String:AnyObject]]) {
  1292. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Resume Pump") }
  1293. // because it's a small array, we're going to destroy and reload every time.
  1294. resumeGraphData.removeAll()
  1295. var lastFoundIndex = 0
  1296. for i in 0..<entries.count {
  1297. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1298. var date: String
  1299. if currentEntry?["timestamp"] != nil {
  1300. date = currentEntry?["timestamp"] as! String
  1301. } else if currentEntry?["created_at"] != nil {
  1302. date = currentEntry?["created_at"] as! String
  1303. } else {
  1304. return
  1305. }
  1306. // Fix for FreeAPS milliseconds in timestamp
  1307. var strippedZone = String(date.dropLast())
  1308. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1309. let dateFormatter = DateFormatter()
  1310. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1311. dateFormatter.locale = Locale(identifier: "en_US")
  1312. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1313. let dateString = dateFormatter.date(from: strippedZone)
  1314. let dateTimeStamp = dateString!.timeIntervalSince1970
  1315. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1316. lastFoundIndex = sgv.foundIndex
  1317. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1318. // Make the dot
  1319. let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1320. resumeGraphData.append(dot)
  1321. }
  1322. }
  1323. if UserDefaultsRepository.graphOtherTreatments.value {
  1324. updateResumeGraph()
  1325. }
  1326. }
  1327. // NS Sensor Start Response Processor
  1328. func processSensorStart(entries: [[String:AnyObject]]) {
  1329. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Sensor Start") }
  1330. // because it's a small array, we're going to destroy and reload every time.
  1331. sensorStartGraphData.removeAll()
  1332. var lastFoundIndex = 0
  1333. for i in 0..<entries.count {
  1334. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1335. var date: String
  1336. if currentEntry?["timestamp"] != nil {
  1337. date = currentEntry?["timestamp"] as! String
  1338. } else if currentEntry?["created_at"] != nil {
  1339. date = currentEntry?["created_at"] as! String
  1340. } else {
  1341. return
  1342. }
  1343. // Fix for FreeAPS milliseconds in timestamp
  1344. var strippedZone = String(date.dropLast())
  1345. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1346. let dateFormatter = DateFormatter()
  1347. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1348. dateFormatter.locale = Locale(identifier: "en_US")
  1349. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1350. let dateString = dateFormatter.date(from: strippedZone)
  1351. let dateTimeStamp = dateString!.timeIntervalSince1970
  1352. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1353. lastFoundIndex = sgv.foundIndex
  1354. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1355. // Make the dot
  1356. let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1357. sensorStartGraphData.append(dot)
  1358. }
  1359. }
  1360. if UserDefaultsRepository.graphOtherTreatments.value {
  1361. updateSensorStart()
  1362. }
  1363. }
  1364. // NS Note Response Processor
  1365. func processNotes(entries: [[String:AnyObject]]) {
  1366. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Notes") }
  1367. // because it's a small array, we're going to destroy and reload every time.
  1368. noteGraphData.removeAll()
  1369. var lastFoundIndex = 0
  1370. for i in 0..<entries.count {
  1371. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1372. var date: String
  1373. if currentEntry?["timestamp"] != nil {
  1374. date = currentEntry?["timestamp"] as! String
  1375. } else if currentEntry?["created_at"] != nil {
  1376. date = currentEntry?["created_at"] as! String
  1377. } else {
  1378. return
  1379. }
  1380. // Fix for FreeAPS milliseconds in timestamp
  1381. var strippedZone = String(date.dropLast())
  1382. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1383. let dateFormatter = DateFormatter()
  1384. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1385. dateFormatter.locale = Locale(identifier: "en_US")
  1386. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1387. let dateString = dateFormatter.date(from: strippedZone)
  1388. let dateTimeStamp = dateString!.timeIntervalSince1970
  1389. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1390. lastFoundIndex = sgv.foundIndex
  1391. guard let thisNote = currentEntry?["notes"] as? String else { continue }
  1392. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1393. // Make the dot
  1394. let dot = DataStructs.noteStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv), note: thisNote)
  1395. noteGraphData.append(dot)
  1396. }
  1397. }
  1398. if UserDefaultsRepository.graphOtherTreatments.value {
  1399. updateNotes()
  1400. }
  1401. }
  1402. // NS BG Check Response Processor
  1403. func processNSBGCheck(entries: [[String:AnyObject]]) {
  1404. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: BG Check") }
  1405. // because it's a small array, we're going to destroy and reload every time.
  1406. bgCheckData.removeAll()
  1407. for i in 0..<entries.count {
  1408. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1409. var date: String
  1410. if currentEntry?["timestamp"] != nil {
  1411. date = currentEntry?["timestamp"] as! String
  1412. } else if currentEntry?["created_at"] != nil {
  1413. date = currentEntry?["created_at"] as! String
  1414. } else {
  1415. return
  1416. }
  1417. // Fix for FreeAPS milliseconds in timestamp
  1418. var strippedZone = String(date.dropLast())
  1419. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1420. let dateFormatter = DateFormatter()
  1421. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1422. dateFormatter.locale = Locale(identifier: "en_US")
  1423. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1424. let dateString = dateFormatter.date(from: strippedZone)
  1425. let dateTimeStamp = dateString!.timeIntervalSince1970
  1426. guard let sgv = currentEntry?["glucose"] as? Int else {
  1427. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Non-Int Glucose entry")}
  1428. continue
  1429. }
  1430. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1431. // Make the dot
  1432. //let dot = ShareGlucoseData(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1433. let dot = ShareGlucoseData(sgv: sgv, date: Double(dateTimeStamp), direction: "")
  1434. bgCheckData.append(dot)
  1435. }
  1436. }
  1437. if UserDefaultsRepository.graphOtherTreatments.value {
  1438. updateBGCheckGraph()
  1439. }
  1440. }
  1441. // NS Override Response Processor
  1442. func processNSOverrides(entries: [[String:AnyObject]]) {
  1443. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Overrides") }
  1444. // because it's a small array, we're going to destroy and reload every time.
  1445. overrideGraphData.removeAll()
  1446. for i in 0..<entries.count {
  1447. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1448. var date: String
  1449. if currentEntry?["timestamp"] != nil {
  1450. date = currentEntry?["timestamp"] as! String
  1451. } else if currentEntry?["created_at"] != nil {
  1452. date = currentEntry?["created_at"] as! String
  1453. } else {
  1454. return
  1455. }
  1456. // Fix for FreeAPS milliseconds in timestamp
  1457. var strippedZone = String(date.dropLast())
  1458. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1459. let dateFormatter = DateFormatter()
  1460. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1461. dateFormatter.locale = Locale(identifier: "en_US")
  1462. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1463. let dateString = dateFormatter.date(from: strippedZone)
  1464. var dateTimeStamp = dateString!.timeIntervalSince1970
  1465. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  1466. if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) {
  1467. dateTimeStamp = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours)
  1468. }
  1469. var multiplier: Double = 1.0
  1470. if currentEntry?["insulinNeedsScaleFactor"] != nil {
  1471. multiplier = currentEntry?["insulinNeedsScaleFactor"] as! Double
  1472. }
  1473. var duration: Double = 5.0
  1474. if let durationType = currentEntry?["durationType"] as? String {
  1475. duration = dateTimeUtils.getNowTimeIntervalUTC() - dateTimeStamp + (60 * 60)
  1476. } else {
  1477. duration = (currentEntry?["duration"] as? Double)!
  1478. duration = duration * 60
  1479. }
  1480. // Skip overrides that aren't 5 minutes long. This prevents overlapping that causes bars to not display.
  1481. if duration < 300 { continue }
  1482. guard let enteredBy = currentEntry?["enteredBy"] as? String else { continue }
  1483. guard let reason = currentEntry?["reason"] as? String else { continue }
  1484. var range: [Int] = []
  1485. if let ranges = currentEntry?["correctionRange"] as? [Int] {
  1486. if ranges.count == 2 {
  1487. guard let low = ranges[0] as? Int else { continue }
  1488. guard let high = ranges[1] as? Int else { continue }
  1489. range.append(low)
  1490. range.append(high)
  1491. }
  1492. }
  1493. let endDate = dateTimeStamp + (duration)
  1494. let dot = DataStructs.overrideStruct(insulNeedsScaleFactor: multiplier, date: dateTimeStamp, endDate: endDate, duration: duration, correctionRange: range, enteredBy: enteredBy, reason: reason, sgv: -20)
  1495. overrideGraphData.append(dot)
  1496. }
  1497. if UserDefaultsRepository.graphOtherTreatments.value {
  1498. updateOverrideGraph()
  1499. }
  1500. }
  1501. }