NightScout.swift 78 KB

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