NightScout.swift 58 KB

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