NightScout.swift 79 KB

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