NightScout.swift 53 KB

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