NightScout.swift 57 KB

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