NightScout.swift 59 KB

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