NightScout.swift 61 KB

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