NightScout.swift 77 KB

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