NightScout.swift 81 KB

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