NightScout.swift 78 KB

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