NightScout.swift 84 KB

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