NightScout.swift 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844
  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 Carbs Struct
  12. struct carbsData: Codable {
  13. var carbs: Double?
  14. }
  15. //NS Cage Struct
  16. struct cageData: Codable {
  17. var created_at: String
  18. }
  19. //NS Basal Profile Struct
  20. struct basalProfileStruct: Codable {
  21. var value: Double
  22. var time: String
  23. var timeAsSeconds: Double
  24. }
  25. //NS Basal Data Struct
  26. struct basalGraphStruct: Codable {
  27. var basalRate: Double
  28. var date: TimeInterval
  29. }
  30. //NS Bolus Data Struct
  31. struct bolusGraphStruct: Codable {
  32. var value: Double
  33. var date: TimeInterval
  34. var sgv: Int
  35. }
  36. //NS Bolus Data Struct
  37. struct carbGraphStruct: Codable {
  38. var value: Double
  39. var date: TimeInterval
  40. var sgv: Int
  41. var absorptionTime: Int
  42. }
  43. func isStaleData() -> Bool {
  44. if bgData.count > 0 {
  45. let now = dateTimeUtils.getNowTimeIntervalUTC()
  46. let lastReadingTime = bgData.last!.date
  47. let secondsAgo = now - lastReadingTime
  48. if secondsAgo >= 20*60 {
  49. return true
  50. } else {
  51. return false
  52. }
  53. } else {
  54. return false
  55. }
  56. }
  57. // Dex Share Web Call
  58. func webLoadDexShare(onlyPullLastRecord: Bool = false) {
  59. // Dexcom Share only returns 24 hrs of data as of now
  60. // Requesting more just for consistency with NS
  61. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  62. var count = graphHours * 12
  63. if onlyPullLastRecord { count = 1 }
  64. dexShare?.fetchData(count) { (err, result) -> () in
  65. // TODO: add error checking
  66. if(err == nil) {
  67. var data = result!
  68. // If Dex data is old, load from NS instead
  69. let latestDate = data[0].date
  70. let now = dateTimeUtils.getNowTimeIntervalUTC()
  71. if (latestDate + 330) < now && UserDefaultsRepository.url.value != "" {
  72. self.webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  73. print("dex didn't load, triggered NS attempt")
  74. return
  75. }
  76. // Dexcom only returns 24 hrs of data. If we need more, call NS.
  77. if graphHours > 24 && !onlyPullLastRecord && UserDefaultsRepository.url.value != "" {
  78. self.webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord, dexData: data)
  79. } else {
  80. self.ProcessDexBGData(data: data, onlyPullLastRecord: onlyPullLastRecord, sourceName: "Dexcom")
  81. }
  82. } else {
  83. // If we get an error, immediately try to pull NS BG Data
  84. if UserDefaultsRepository.url.value != "" {
  85. self.webLoadNSBGData(onlyPullLastRecord: onlyPullLastRecord)
  86. }
  87. if globalVariables.dexVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  88. globalVariables.dexVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  89. DispatchQueue.main.async {
  90. //self.sendNotification(title: "Dexcom Share Error", body: "Please double check user name and password, internet connection, and sharing status.")
  91. }
  92. }
  93. }
  94. }
  95. }
  96. // NS BG Data Web call
  97. func webLoadNSBGData(onlyPullLastRecord: Bool = false, dexData: [ShareGlucoseData] = []) {
  98. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: BG") }
  99. // This kicks it out in the instance where dexcom fails but they aren't using NS &&
  100. if UserDefaultsRepository.url.value == "" {
  101. self.startBGTimer(time: 10)
  102. return
  103. }
  104. // URL processor
  105. var urlBGDataPath: String = UserDefaultsRepository.url.value + "/api/v1/entries/sgv.json?"
  106. if onlyPullLastRecord {
  107. urlBGDataPath = urlBGDataPath + "count=1"
  108. } else {
  109. //Fetch entries for the time period of "downloadDays"
  110. let utcISODateFormatter = ISO8601DateFormatter()
  111. let date = Calendar.current.date(byAdding: .day, value: -1 * UserDefaultsRepository.downloadDays.value, to: Date())!
  112. urlBGDataPath = urlBGDataPath + "count=1000&find[dateString][$gte]=" + utcISODateFormatter.string(from: date)
  113. }
  114. if !token.isEmpty {
  115. urlBGDataPath = urlBGDataPath + "&token=" + token
  116. }
  117. guard let urlBGData = URL(string: urlBGDataPath) else {
  118. // if we have Dex data, use it
  119. if !dexData.isEmpty {
  120. self.ProcessDexBGData(data: dexData, onlyPullLastRecord: onlyPullLastRecord, sourceName: "Dexcom")
  121. return
  122. }
  123. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  124. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  125. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  126. }
  127. DispatchQueue.main.async {
  128. if self.bgTimer.isValid {
  129. self.bgTimer.invalidate()
  130. }
  131. self.startBGTimer(time: 10)
  132. }
  133. return
  134. }
  135. var request = URLRequest(url: urlBGData)
  136. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  137. // Downloader
  138. let getBGTask = URLSession.shared.dataTask(with: request) { data, response, error in
  139. guard error == nil else {
  140. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  141. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  142. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  143. }
  144. DispatchQueue.main.async {
  145. if self.bgTimer.isValid {
  146. self.bgTimer.invalidate()
  147. }
  148. self.startBGTimer(time: 10)
  149. }
  150. // if we have Dex data, use it
  151. if !dexData.isEmpty {
  152. self.ProcessDexBGData(data: dexData, onlyPullLastRecord: onlyPullLastRecord, sourceName: "Dexcom")
  153. }
  154. return
  155. }
  156. guard let data = data else {
  157. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  158. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  159. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  160. }
  161. DispatchQueue.main.async {
  162. if self.bgTimer.isValid {
  163. self.bgTimer.invalidate()
  164. }
  165. self.startBGTimer(time: 10)
  166. }
  167. return
  168. }
  169. let decoder = JSONDecoder()
  170. let entriesResponse = try? decoder.decode([ShareGlucoseData].self, from: data)
  171. if var nsData = entriesResponse {
  172. DispatchQueue.main.async {
  173. // transform NS data to look like Dex data
  174. for i in 0..<nsData.count {
  175. // convert the NS timestamp to seconds instead of milliseconds
  176. nsData[i].date /= 1000
  177. nsData[i].date.round(FloatingPointRoundingRule.toNearestOrEven)
  178. }
  179. print(nsData.count)
  180. //Avoid duplicate entries messing up the graph, only use one reading per 5 minutes.
  181. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  182. let points = graphHours * 12 + 1
  183. var nsData2 = [ShareGlucoseData]()
  184. let timestamp = Date().timeIntervalSince1970
  185. for i in 0..<points {
  186. //Starting with "now" and then step 5 minutes back in time
  187. let target = timestamp - Double(i) * 60 * 5
  188. //Find the reading closest to the target, but not too far away
  189. let closest = nsData.filter{ abs($0.date - target) < 3 * 60 }.min { abs($0.date - target) < abs($1.date - target) }
  190. //If a reading is found, add it to the new array
  191. if let item = closest {
  192. nsData2.append(item)
  193. }
  194. }
  195. print(nsData2.count)
  196. // merge NS and Dex data if needed; use recent Dex data and older NS data
  197. var sourceName = "Nightscout"
  198. if !dexData.isEmpty {
  199. let oldestDexDate = dexData[dexData.count - 1].date
  200. var itemsToRemove = 0
  201. while itemsToRemove < nsData2.count && nsData2[itemsToRemove].date >= oldestDexDate {
  202. itemsToRemove += 1
  203. }
  204. nsData2.removeFirst(itemsToRemove)
  205. nsData2 = dexData + nsData2
  206. sourceName = "Dexcom"
  207. }
  208. // trigger the processor for the data after downloading.
  209. self.ProcessDexBGData(data: nsData2, onlyPullLastRecord: onlyPullLastRecord, sourceName: sourceName)
  210. }
  211. } else {
  212. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  213. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  214. //self.sendNotification(title: "Nightscout Failure", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  215. }
  216. DispatchQueue.main.async {
  217. if self.bgTimer.isValid {
  218. self.bgTimer.invalidate()
  219. }
  220. self.startBGTimer(time: 10)
  221. }
  222. return
  223. }
  224. }
  225. getBGTask.resume()
  226. }
  227. // Dexcom BG Data Response processor
  228. func ProcessDexBGData(data: [ShareGlucoseData], onlyPullLastRecord: Bool, sourceName: String){
  229. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: BG") }
  230. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  231. let pullDate = data[data.count - 1].date
  232. let latestDate = data[0].date
  233. let now = dateTimeUtils.getNowTimeIntervalUTC()
  234. // Start the BG timer based on the reading
  235. let secondsAgo = now - latestDate
  236. DispatchQueue.main.async {
  237. // if reading is overdue over: 20:00, re-attempt every 5 minutes
  238. if secondsAgo >= (20 * 60) {
  239. self.startBGTimer(time: (5 * 60))
  240. print("##### started 5 minute bg timer")
  241. // if the reading is overdue: 10:00-19:59, re-attempt every minute
  242. } else if secondsAgo >= (10 * 60) {
  243. self.startBGTimer(time: 60)
  244. print("##### started 1 minute bg timer")
  245. // if the reading is overdue: 7:00-9:59, re-attempt every 30 seconds
  246. } else if secondsAgo >= (7 * 60) {
  247. self.startBGTimer(time: 30)
  248. print("##### started 30 second bg timer")
  249. // if the reading is overdue: 5:00-6:59 re-attempt every 10 seconds
  250. } else if secondsAgo >= (5 * 60) {
  251. self.startBGTimer(time: 10)
  252. print("##### started 10 second bg timer")
  253. // We have a current reading. Set timer to 5:10 from last reading
  254. } else {
  255. self.startBGTimer(time: 300 - secondsAgo + Double(UserDefaultsRepository.bgUpdateDelay.value))
  256. let timerVal = 310 - secondsAgo
  257. print("##### started 5:10 bg timer: \(timerVal)")
  258. self.updateBadge(val: data[0].sgv)
  259. if UserDefaultsRepository.speakBG.value {
  260. self.speakBG(currentValue: data[0].sgv, previousValue: data[1].sgv)
  261. }
  262. }
  263. }
  264. // 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
  265. if !onlyPullLastRecord {
  266. bgData.removeAll()
  267. } else if bgData[bgData.count - 1].date != pullDate {
  268. bgData.removeFirst()
  269. } else {
  270. return
  271. }
  272. // loop through the data so we can reverse the order to oldest first for the graph
  273. for i in 0..<data.count{
  274. let dateString = data[data.count - 1 - i].date
  275. if dateString >= dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) {
  276. let reading = ShareGlucoseData(sgv: data[data.count - 1 - i].sgv, date: dateString, direction: data[data.count - 1 - i].direction)
  277. bgData.append(reading)
  278. }
  279. }
  280. viewUpdateNSBG(sourceName: sourceName)
  281. }
  282. // NS BG Data Front end updater
  283. func viewUpdateNSBG (sourceName: String) {
  284. DispatchQueue.main.async {
  285. if UserDefaultsRepository.debugLog.value {
  286. self.writeDebugLog(value: "Display: BG")
  287. self.writeDebugLog(value: "Num BG: " + self.bgData.count.description)
  288. }
  289. let entries = self.bgData
  290. if entries.count < 1 { return }
  291. self.updateBGGraph()
  292. self.updateStats()
  293. let latestEntryi = entries.count - 1
  294. let latestBG = entries[latestEntryi].sgv
  295. let priorBG = entries[latestEntryi - 1].sgv
  296. let deltaBG = latestBG - priorBG as Int
  297. let lastBGTime = entries[latestEntryi].date
  298. let deltaTime = (TimeInterval(Date().timeIntervalSince1970)-lastBGTime) / 60
  299. var userUnit = " mg/dL"
  300. if self.mmol {
  301. userUnit = " mmol/L"
  302. }
  303. self.serverText.text = sourceName
  304. var snoozerBG = ""
  305. var snoozerDirection = ""
  306. var snoozerDelta = ""
  307. self.BGText.text = bgUnits.toDisplayUnits(String(latestBG))
  308. snoozerBG = bgUnits.toDisplayUnits(String(latestBG))
  309. self.setBGTextColor()
  310. if let directionBG = entries[latestEntryi].direction {
  311. self.DirectionText.text = self.bgDirectionGraphic(directionBG)
  312. snoozerDirection = self.bgDirectionGraphic(directionBG)
  313. self.latestDirectionString = self.bgDirectionGraphic(directionBG)
  314. }
  315. else
  316. {
  317. self.DirectionText.text = ""
  318. snoozerDirection = ""
  319. self.latestDirectionString = ""
  320. }
  321. if deltaBG < 0 {
  322. self.DeltaText.text = bgUnits.toDisplayUnits(String(deltaBG))
  323. snoozerDelta = bgUnits.toDisplayUnits(String(deltaBG))
  324. self.latestDeltaString = String(deltaBG)
  325. }
  326. else
  327. {
  328. self.DeltaText.text = "+" + bgUnits.toDisplayUnits(String(deltaBG))
  329. snoozerDelta = "+" + bgUnits.toDisplayUnits(String(deltaBG))
  330. self.latestDeltaString = "+" + String(deltaBG)
  331. }
  332. // Snoozer Display
  333. guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return }
  334. snoozer.BGLabel.text = snoozerBG
  335. snoozer.DirectionLabel.text = snoozerDirection
  336. snoozer.DeltaLabel.text = snoozerDelta
  337. }
  338. }
  339. // NS Device Status Web Call
  340. func webLoadNSDeviceStatus() {
  341. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: device status") }
  342. let urlUser = UserDefaultsRepository.url.value
  343. // NS Api is not working to find by greater than date
  344. var urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=288"
  345. if token != "" {
  346. urlStringDeviceStatus = urlUser + "/api/v1/devicestatus.json?count=288&token=" + token
  347. }
  348. let escapedAddress = urlStringDeviceStatus.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  349. guard let urlDeviceStatus = URL(string: escapedAddress!) else {
  350. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  351. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  352. //self.sendNotification(title: "Nightscout Failure", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  353. }
  354. DispatchQueue.main.async {
  355. if self.deviceStatusTimer.isValid {
  356. self.deviceStatusTimer.invalidate()
  357. }
  358. self.startDeviceStatusTimer(time: 10)
  359. }
  360. return
  361. }
  362. var requestDeviceStatus = URLRequest(url: urlDeviceStatus)
  363. requestDeviceStatus.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  364. let deviceStatusTask = URLSession.shared.dataTask(with: requestDeviceStatus) { data, response, error in
  365. guard error == nil else {
  366. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  367. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  368. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  369. }
  370. DispatchQueue.main.async {
  371. if self.deviceStatusTimer.isValid {
  372. self.deviceStatusTimer.invalidate()
  373. }
  374. self.startDeviceStatusTimer(time: 10)
  375. }
  376. return
  377. }
  378. guard let data = data else {
  379. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  380. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  381. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  382. }
  383. DispatchQueue.main.async {
  384. if self.deviceStatusTimer.isValid {
  385. self.deviceStatusTimer.invalidate()
  386. }
  387. self.startDeviceStatusTimer(time: 10)
  388. }
  389. return
  390. }
  391. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  392. if let json = json {
  393. DispatchQueue.main.async {
  394. self.updateDeviceStatusDisplay(jsonDeviceStatus: json)
  395. }
  396. } else {
  397. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  398. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  399. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  400. }
  401. DispatchQueue.main.async {
  402. if self.deviceStatusTimer.isValid {
  403. self.deviceStatusTimer.invalidate()
  404. }
  405. self.startDeviceStatusTimer(time: 10)
  406. }
  407. return
  408. }
  409. }
  410. deviceStatusTask.resume()
  411. }
  412. // NS Device Status Response Processor
  413. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  414. self.clearLastInfoData(index: 0)
  415. self.clearLastInfoData(index: 1)
  416. self.clearLastInfoData(index: 3)
  417. self.clearLastInfoData(index: 4)
  418. self.clearLastInfoData(index: 5)
  419. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
  420. if jsonDeviceStatus.count == 0 {
  421. return
  422. }
  423. //Process the current data first
  424. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  425. //pump and uploader
  426. let formatter = ISO8601DateFormatter()
  427. formatter.formatOptions = [.withFullDate,
  428. .withTime,
  429. .withDashSeparatorInDate,
  430. .withColonSeparatorInTime]
  431. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  432. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  433. if let reservoirData = lastPumpRecord["reservoir"] as? Double {
  434. latestPumpVolume = reservoirData
  435. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  436. } else {
  437. latestPumpVolume = 50.0
  438. tableData[5].value = "50+U"
  439. }
  440. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  441. let upbat = uploader["battery"] as! Double
  442. tableData[4].value = String(format:"%.0f", upbat) + "%"
  443. }
  444. }
  445. }
  446. // Loop
  447. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  448. //print("Loop: \(lastLoopRecord)")
  449. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  450. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  451. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  452. if let failure = lastLoopRecord["failureReason"] {
  453. LoopStatusLabel.text = "X"
  454. latestLoopStatusString = "X"
  455. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  456. } else {
  457. var wasEnacted = false
  458. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  459. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  460. wasEnacted = true
  461. if let lastTempBasal = enacted["rate"] as? Double {
  462. }
  463. }
  464. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  465. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  466. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  467. }
  468. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  469. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  470. latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
  471. }
  472. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  473. let prediction = predictdata["values"] as! [Double]
  474. PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
  475. PredictionLabel.textColor = UIColor.systemPurple
  476. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  477. predictionData.removeAll()
  478. var predictionTime = lastLoopTime
  479. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  480. var i = 0
  481. while i <= toLoad {
  482. if i < prediction.count {
  483. let prediction = ShareGlucoseData(sgv: Int(round(prediction[i])), date: predictionTime, direction: "flat")
  484. predictionData.append(prediction)
  485. predictionTime += 300
  486. }
  487. i += 1
  488. }
  489. let predMin = prediction.min()
  490. let predMax = prediction.max()
  491. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  492. updatePredictionGraph()
  493. }
  494. }
  495. if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
  496. tableData[8].value = String(format:"%.2fU", recBolus)
  497. }
  498. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  499. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  500. var lastBGTime = lastLoopTime
  501. if bgData.count > 0 {
  502. lastBGTime = bgData[bgData.count - 1].date
  503. }
  504. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  505. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  506. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  507. if tempBasalTime > lastBGTime && !wasEnacted {
  508. LoopStatusLabel.text = "⏀"
  509. latestLoopStatusString = "⏀"
  510. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  511. } else {
  512. LoopStatusLabel.text = "↻"
  513. latestLoopStatusString = "↻"
  514. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  515. }
  516. }
  517. } else {
  518. LoopStatusLabel.text = "↻"
  519. latestLoopStatusString = "↻"
  520. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  521. }
  522. }
  523. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 {
  524. LoopStatusLabel.text = "⚠"
  525. latestLoopStatusString = "⚠"
  526. }
  527. latestLoopTime = lastLoopTime
  528. } // end lastLoopTime
  529. } // end lastLoop Record
  530. var oText = "" as String
  531. currentOverride = 1.0
  532. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  533. if let lastOverrideTime = formatter.date(from: (lastOverride["timestamp"] as! String))?.timeIntervalSince1970 {
  534. }
  535. if lastOverride["active"] as! Bool {
  536. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  537. if let multiplier = lastOverride["multiplier"] as? Double {
  538. currentOverride = multiplier
  539. oText += String(format: "%.0f%%", (multiplier * 100))
  540. }
  541. else
  542. {
  543. oText += "100%"
  544. }
  545. oText += " ("
  546. let minValue = lastCorrection["minValue"] as! Double
  547. let maxValue = lastCorrection["maxValue"] as! Double
  548. oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
  549. tableData[3].value = oText
  550. }
  551. }
  552. infoTable.reloadData()
  553. // Start the timer based on the timestamp
  554. let now = dateTimeUtils.getNowTimeIntervalUTC()
  555. let secondsAgo = now - latestLoopTime
  556. DispatchQueue.main.async {
  557. // if Loop is overdue over: 20:00, re-attempt every 5 minutes
  558. if secondsAgo >= (20 * 60) {
  559. self.startDeviceStatusTimer(time: (5 * 60))
  560. print("started 5 minute device status timer")
  561. // if the Loop is overdue: 10:00-19:59, re-attempt every minute
  562. } else if secondsAgo >= (10 * 60) {
  563. self.startDeviceStatusTimer(time: 60)
  564. print("started 1 minute device status timer")
  565. // if the Loop is overdue: 7:00-9:59, re-attempt every 30 seconds
  566. } else if secondsAgo >= (7 * 60) {
  567. self.startDeviceStatusTimer(time: 30)
  568. print("started 30 second device status timer")
  569. // if the Loop is overdue: 5:00-6:59 re-attempt every 10 seconds
  570. } else if secondsAgo >= (5 * 60) {
  571. self.startDeviceStatusTimer(time: 10)
  572. print("started 10 second device status timer")
  573. // We have a current Loop. Set timer to 5:10 from last reading
  574. } else {
  575. self.startDeviceStatusTimer(time: 310 - secondsAgo)
  576. let timerVal = 310 - secondsAgo
  577. print("started 5:10 device status timer: \(timerVal)")
  578. }
  579. }
  580. }
  581. // NS Carbs Today Web Call
  582. func webLoadNSCarbsToday() {
  583. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Carbs Today") }
  584. let urlUser = UserDefaultsRepository.url.value
  585. let now = Date()
  586. let timeZone = TimeZone.current
  587. var calendar = Calendar(identifier: .gregorian)
  588. calendar.timeZone = timeZone
  589. var dateComponents = calendar.dateComponents(in: timeZone, from: now)
  590. dateComponents.hour = 0
  591. dateComponents.minute = 0
  592. dateComponents.second = 0
  593. guard let date = dateComponents.date else { fatalError("Invalid date components") }
  594. let dateFormatter = DateFormatter()
  595. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
  596. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  597. let utcDateString = dateFormatter.string(from: date)
  598. var urlString = urlUser + "/api/v1/treatments.json?count=1000&find[eventType]=Carb+Correction&find[timestamp][$gte]=" + utcDateString
  599. if token != "" {
  600. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&count=1000&find[eventType]=Carb+Correction&find[timestamp][$gte]=" + utcDateString
  601. }
  602. guard let urlData = URL(string: urlString) else {
  603. return
  604. }
  605. var request = URLRequest(url: urlData)
  606. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  607. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  608. guard error == nil else {
  609. return
  610. }
  611. guard let data = data else {
  612. return
  613. }
  614. let decoder = JSONDecoder()
  615. let entriesResponse = try? decoder.decode([carbsData].self, from: data)
  616. if let entriesResponse = entriesResponse {
  617. DispatchQueue.main.async {
  618. self.updateCarbsToday(data: entriesResponse)
  619. }
  620. } else {
  621. return
  622. }
  623. }
  624. task.resume()
  625. }
  626. // NS CarbsToday Response Processor
  627. func updateCarbsToday(data: [carbsData]) {
  628. self.clearLastInfoData(index: 10)
  629. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: carbs") }
  630. if data.count == 0 {
  631. return
  632. }
  633. let totalCarbs = data.reduce(0.0) { $0 + ($1.carbs ?? 0.0) }
  634. let resultString = String(format: "%.0f", totalCarbs)
  635. tableData[10].value = resultString
  636. infoTable.reloadData()
  637. }
  638. // NS Cage Web Call
  639. func webLoadNSCage() {
  640. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: CAGE") }
  641. let urlUser = UserDefaultsRepository.url.value
  642. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Site%20Change&count=1"
  643. if token != "" {
  644. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Site%20Change&count=1"
  645. }
  646. guard let urlData = URL(string: urlString) else {
  647. return
  648. }
  649. var request = URLRequest(url: urlData)
  650. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  651. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  652. guard error == nil else {
  653. return
  654. }
  655. guard let data = data else {
  656. return
  657. }
  658. let decoder = JSONDecoder()
  659. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  660. if let entriesResponse = entriesResponse {
  661. DispatchQueue.main.async {
  662. self.updateCage(data: entriesResponse)
  663. }
  664. } else {
  665. return
  666. }
  667. }
  668. task.resume()
  669. }
  670. // NS Cage Response Processor
  671. func updateCage(data: [cageData]) {
  672. self.clearLastInfoData(index: 7)
  673. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: CAGE") }
  674. if data.count == 0 {
  675. return
  676. }
  677. let lastCageString = data[0].created_at
  678. let formatter = ISO8601DateFormatter()
  679. formatter.formatOptions = [.withFullDate,
  680. .withTime,
  681. .withDashSeparatorInDate,
  682. .withColonSeparatorInTime]
  683. UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval
  684. if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 {
  685. let now = dateTimeUtils.getNowTimeIntervalUTC()
  686. let secondsAgo = now - cageTime
  687. //let days = 24 * 60 * 60
  688. let formatter = DateComponentsFormatter()
  689. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  690. formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string
  691. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  692. let formattedDuration = formatter.string(from: secondsAgo)
  693. tableData[7].value = formattedDuration ?? ""
  694. }
  695. infoTable.reloadData()
  696. }
  697. // NS Sage Web Call
  698. func webLoadNSSage() {
  699. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: SAGE") }
  700. let lastDateString = dateTimeUtils.nowMinus10DaysTimeInterval()
  701. let urlUser = UserDefaultsRepository.url.value
  702. var urlString = urlUser + "/api/v1/treatments.json?find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  703. if token != "" {
  704. urlString = urlUser + "/api/v1/treatments.json?token=" + token + "&find[eventType]=Sensor%20Start&find[created_at][$gte]=" + lastDateString + "&count=1"
  705. }
  706. guard let urlData = URL(string: urlString) else {
  707. return
  708. }
  709. var request = URLRequest(url: urlData)
  710. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  711. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  712. guard error == nil else {
  713. return
  714. }
  715. guard let data = data else {
  716. return
  717. }
  718. let decoder = JSONDecoder()
  719. let entriesResponse = try? decoder.decode([cageData].self, from: data)
  720. if let entriesResponse = entriesResponse {
  721. DispatchQueue.main.async {
  722. self.updateSage(data: entriesResponse)
  723. }
  724. } else {
  725. return
  726. }
  727. }
  728. task.resume()
  729. }
  730. // NS Sage Response Processor
  731. func updateSage(data: [cageData]) {
  732. self.clearLastInfoData(index: 6)
  733. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process/Display: SAGE") }
  734. if data.count == 0 {
  735. return
  736. }
  737. var lastSageString = data[0].created_at
  738. let formatter = ISO8601DateFormatter()
  739. formatter.formatOptions = [.withFullDate,
  740. .withTime,
  741. .withDashSeparatorInDate,
  742. .withColonSeparatorInTime]
  743. UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: (lastSageString))?.timeIntervalSince1970 as! TimeInterval
  744. if UserDefaultsRepository.alertAutoSnoozeCGMStart.value && (dateTimeUtils.getNowTimeIntervalUTC() - UserDefaultsRepository.alertSageInsertTime.value < 7200){
  745. let snoozeTime = Date(timeIntervalSince1970: UserDefaultsRepository.alertSageInsertTime.value + 7200)
  746. UserDefaultsRepository.alertSnoozeAllTime.value = snoozeTime
  747. UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = true
  748. guard let alarms = self.tabBarController!.viewControllers?[1] as? AlarmViewController else { return }
  749. alarms.reloadIsSnoozed(key: "alertSnoozeAllIsSnoozed", value: true)
  750. alarms.reloadSnoozeTime(key: "alertSnoozeAllTime", setNil: false, value: snoozeTime)
  751. }
  752. if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 {
  753. let now = dateTimeUtils.getNowTimeIntervalUTC()
  754. let secondsAgo = now - sageTime
  755. let days = 24 * 60 * 60
  756. let formatter = DateComponentsFormatter()
  757. formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale
  758. formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string
  759. formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale
  760. let formattedDuration = formatter.string(from: secondsAgo)
  761. tableData[6].value = formattedDuration ?? ""
  762. }
  763. infoTable.reloadData()
  764. }
  765. // NS Profile Web Call
  766. func webLoadNSProfile() {
  767. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: profile") }
  768. let urlUser = UserDefaultsRepository.url.value
  769. var urlString = urlUser + "/api/v1/profile/current.json"
  770. if token != "" {
  771. urlString = urlUser + "/api/v1/profile/current.json?token=" + token
  772. }
  773. let escapedAddress = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
  774. guard let url = URL(string: escapedAddress!) else {
  775. return
  776. }
  777. var request = URLRequest(url: url)
  778. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  779. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  780. guard error == nil else {
  781. return
  782. }
  783. guard let data = data else {
  784. return
  785. }
  786. let json = try? JSONSerialization.jsonObject(with: data) as! Dictionary<String, Any>
  787. if let json = json {
  788. DispatchQueue.main.async {
  789. self.updateProfile(jsonDeviceStatus: json)
  790. }
  791. } else {
  792. return
  793. }
  794. }
  795. task.resume()
  796. }
  797. // NS Profile Response Processor
  798. func updateProfile(jsonDeviceStatus: Dictionary<String, Any>) {
  799. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: profile") }
  800. if jsonDeviceStatus.count == 0 {
  801. return
  802. }
  803. if jsonDeviceStatus[keyPath: "message"] != nil { return }
  804. let basal = try jsonDeviceStatus[keyPath: "store.Default.basal"] as! NSArray
  805. basalProfile.removeAll()
  806. for i in 0..<basal.count {
  807. let dict = basal[i] as! Dictionary<String, Any>
  808. do {
  809. let thisValue = try dict[keyPath: "value"] as! Double
  810. let thisTime = dict[keyPath: "time"] as! String
  811. let thisTimeAsSeconds = dict[keyPath: "timeAsSeconds"] as! Double
  812. let entry = basalProfileStruct(value: thisValue, time: thisTime, timeAsSeconds: thisTimeAsSeconds)
  813. basalProfile.append(entry)
  814. } catch {
  815. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: profile wrapped in quotes") }
  816. }
  817. }
  818. // Don't process the basal or draw the graph until after the BG has been fully processeed and drawn
  819. if firstGraphLoad { return }
  820. var basalSegments: [DataStructs.basalProfileSegment] = []
  821. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  822. // Build scheduled basal segments from right to left by
  823. // moving pointers to the current midnight and current basal
  824. var midnight = dateTimeUtils.getTimeIntervalMidnightToday()
  825. var basalProfileIndex = basalProfile.count - 1
  826. var start = midnight + basalProfile[basalProfileIndex].timeAsSeconds
  827. var end = dateTimeUtils.getNowTimeIntervalUTC()
  828. // Move back until we're in the graph range
  829. while start > end {
  830. basalProfileIndex -= 1
  831. start = midnight + basalProfile[basalProfileIndex].timeAsSeconds
  832. }
  833. // Add records while they're still within the graph
  834. let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours)
  835. while end >= graphStart {
  836. let entry = DataStructs.basalProfileSegment(
  837. basalRate: basalProfile[basalProfileIndex].value, startDate: start, endDate: end)
  838. basalSegments.append(entry)
  839. basalProfileIndex -= 1
  840. if basalProfileIndex < 0 {
  841. basalProfileIndex = basalProfile.count - 1
  842. midnight = midnight.advanced(by: -24*60*60)
  843. }
  844. end = start - 1
  845. start = midnight + basalProfile[basalProfileIndex].timeAsSeconds
  846. }
  847. // reverse the result to get chronological order
  848. basalSegments.reverse()
  849. var firstPass = true
  850. // Runs the scheduled basal to the end of the prediction line
  851. var predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * UserDefaultsRepository.predictionToLoad.value)
  852. basalScheduleData.removeAll()
  853. for i in 0..<basalSegments.count {
  854. let timeStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours)
  855. // This processed everything after the first one.
  856. if firstPass == false
  857. && basalSegments[i].startDate <= predictionEndTime {
  858. let startDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: basalSegments[i].startDate)
  859. basalScheduleData.append(startDot)
  860. var endDate = basalSegments[i].endDate
  861. // if it's the last one needed, set it to end at the prediction end time
  862. if endDate > predictionEndTime || i == basalSegments.count - 1 {
  863. endDate = Double(predictionEndTime)
  864. }
  865. let endDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: endDate)
  866. basalScheduleData.append(endDot)
  867. }
  868. // we need to manually set the first one
  869. // Check that this is the first one and there are no existing entries
  870. if firstPass == true {
  871. // check that the timestamp is > the current entry and < the next entry
  872. if timeStart >= basalSegments[i].startDate && timeStart < basalSegments[i].endDate {
  873. // Set the start time to match the BG start
  874. let startDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: Double(timeStart + (60 * 5)))
  875. basalScheduleData.append(startDot)
  876. // set the enddot where the next one will start
  877. var endDate = basalSegments[i].endDate
  878. let endDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: endDate)
  879. basalScheduleData.append(endDot)
  880. firstPass = false
  881. }
  882. }
  883. }
  884. if UserDefaultsRepository.graphBasal.value {
  885. updateBasalScheduledGraph()
  886. }
  887. }
  888. // NS Treatments Web Call
  889. // Downloads Basal, Bolus, Carbs, BG Check, Notes, Overrides
  890. func WebLoadNSTreatments() {
  891. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Download: Treatments") }
  892. if !UserDefaultsRepository.downloadTreatments.value { return }
  893. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  894. let startTimeString = dateTimeUtils.nowMinusNHoursTimeInterval(N: graphHours)
  895. var urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?find[created_at][$gte]=" + startTimeString
  896. if token != "" {
  897. urlString = UserDefaultsRepository.url.value + "/api/v1/treatments.json?token=" + token + "&find[created_at][$gte]=" + startTimeString
  898. }
  899. guard let urlData = URL(string: urlString) else {
  900. return
  901. }
  902. var request = URLRequest(url: urlData)
  903. request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  904. let task = URLSession.shared.dataTask(with: request) { data, response, error in
  905. guard error == nil else {
  906. return
  907. }
  908. guard let data = data else {
  909. return
  910. }
  911. let json = try? (JSONSerialization.jsonObject(with: data) as? [[String:AnyObject]])
  912. if let json = json {
  913. DispatchQueue.main.async {
  914. self.updateTreatments(entries: json)
  915. }
  916. } else {
  917. return
  918. }
  919. }
  920. task.resume()
  921. }
  922. // Process and split out treatments to individual tasks
  923. func updateTreatments(entries: [[String:AnyObject]]) {
  924. var tempBasal: [[String:AnyObject]] = []
  925. var bolus: [[String:AnyObject]] = []
  926. var carbs: [[String:AnyObject]] = []
  927. var temporaryOverride: [[String:AnyObject]] = []
  928. var note: [[String:AnyObject]] = []
  929. var bgCheck: [[String:AnyObject]] = []
  930. var suspendPump: [[String:AnyObject]] = []
  931. var resumePump: [[String:AnyObject]] = []
  932. var pumpSiteChange: [[String:AnyObject]] = []
  933. var cgmSensorStart: [[String:AnyObject]] = []
  934. for i in 0..<entries.count {
  935. let entry = entries[i] as [String : AnyObject]?
  936. switch entry?["eventType"] as! String {
  937. case "Temp Basal":
  938. tempBasal.append(entry!)
  939. case "Correction Bolus":
  940. bolus.append(entry!)
  941. case "Meal Bolus":
  942. carbs.append(entry!)
  943. bolus.append(entry!)
  944. case "Carb Correction":
  945. carbs.append(entry!)
  946. case "Temporary Override":
  947. temporaryOverride.append(entry!)
  948. case "Note":
  949. note.append(entry!)
  950. print("Note: \(String(describing: entry))")
  951. case "BG Check":
  952. bgCheck.append(entry!)
  953. case "Suspend Pump":
  954. suspendPump.append(entry!)
  955. case "Resume Pump":
  956. resumePump.append(entry!)
  957. case "Pump Site Change":
  958. pumpSiteChange.append(entry!)
  959. case "Sensor Start":
  960. cgmSensorStart.append(entry!)
  961. default:
  962. print("No Match: \(String(describing: entry))")
  963. }
  964. }
  965. // end of for loop
  966. if tempBasal.count > 0 {
  967. processNSBasals(entries: tempBasal)
  968. } else {
  969. if basalData.count < 0 {
  970. clearOldTempBasal()
  971. }
  972. }
  973. if bolus.count > 0 {
  974. processNSBolus(entries: bolus)
  975. } else {
  976. if bolusData.count > 0 {
  977. clearOldBolus()
  978. }
  979. }
  980. if carbs.count > 0 {
  981. processNSCarbs(entries: carbs)
  982. } else {
  983. if carbData.count > 0 {
  984. clearOldCarb()
  985. }
  986. }
  987. if bgCheck.count > 0 {
  988. processNSBGCheck(entries: bgCheck)
  989. } else {
  990. if bgCheckData.count > 0 {
  991. clearOldBGCheck()
  992. }
  993. }
  994. if temporaryOverride.count > 0 {
  995. processNSOverrides(entries: temporaryOverride)
  996. } else {
  997. if overrideGraphData.count > 0 {
  998. clearOldOverride()
  999. }
  1000. }
  1001. if suspendPump.count > 0 {
  1002. processSuspendPump(entries: suspendPump)
  1003. } else {
  1004. if suspendGraphData.count > 0 {
  1005. clearOldSuspend()
  1006. }
  1007. }
  1008. if resumePump.count > 0 {
  1009. processResumePump(entries: resumePump)
  1010. } else {
  1011. if resumeGraphData.count > 0 {
  1012. clearOldResume()
  1013. }
  1014. }
  1015. if cgmSensorStart.count > 0 {
  1016. processSensorStart(entries: cgmSensorStart)
  1017. } else {
  1018. if sensorStartGraphData.count > 0 {
  1019. clearOldSensor()
  1020. }
  1021. }
  1022. if note.count > 0 {
  1023. processNotes(entries: note)
  1024. } else {
  1025. if noteGraphData.count > 0 {
  1026. clearOldNotes()
  1027. }
  1028. }
  1029. }
  1030. func clearOldTempBasal()
  1031. {
  1032. basalData.removeAll()
  1033. updateBasalGraph()
  1034. }
  1035. func clearOldBolus()
  1036. {
  1037. bolusData.removeAll()
  1038. updateBolusGraph()
  1039. }
  1040. func clearOldCarb()
  1041. {
  1042. carbData.removeAll()
  1043. updateCarbGraph()
  1044. }
  1045. func clearOldBGCheck()
  1046. {
  1047. bgCheckData.removeAll()
  1048. updateBGCheckGraph()
  1049. }
  1050. func clearOldOverride()
  1051. {
  1052. overrideGraphData.removeAll()
  1053. updateOverrideGraph()
  1054. }
  1055. func clearOldSuspend()
  1056. {
  1057. suspendGraphData.removeAll()
  1058. updateSuspendGraph()
  1059. }
  1060. func clearOldResume()
  1061. {
  1062. resumeGraphData.removeAll()
  1063. updateResumeGraph()
  1064. }
  1065. func clearOldSensor()
  1066. {
  1067. sensorStartGraphData.removeAll()
  1068. updateSensorStart()
  1069. }
  1070. func clearOldNotes()
  1071. {
  1072. noteGraphData.removeAll()
  1073. updateNotes()
  1074. }
  1075. // NS Temp Basal Response Processor
  1076. func processNSBasals(entries: [[String:AnyObject]]) {
  1077. self.clearLastInfoData(index: 2)
  1078. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") }
  1079. // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
  1080. basalData.removeAll()
  1081. var lastEndDot = 0.0
  1082. var tempArray = entries
  1083. tempArray.reverse()
  1084. for i in 0..<tempArray.count {
  1085. let currentEntry = tempArray[i] as [String : AnyObject]?
  1086. var basalDate: String
  1087. if currentEntry?["timestamp"] != nil {
  1088. basalDate = currentEntry?["timestamp"] as! String
  1089. } else if currentEntry?["created_at"] != nil {
  1090. basalDate = currentEntry?["created_at"] as! String
  1091. } else {
  1092. continue
  1093. }
  1094. var strippedZone = String(basalDate.dropLast())
  1095. strippedZone = strippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
  1096. let dateFormatter = DateFormatter()
  1097. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1098. dateFormatter.locale = Locale(identifier: "en_US")
  1099. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1100. guard let dateString = dateFormatter.date(from: strippedZone) else { continue }
  1101. let dateTimeStamp = dateString.timeIntervalSince1970
  1102. guard let basalRate = currentEntry?["absolute"] as? Double else {
  1103. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Basal entry")}
  1104. continue
  1105. }
  1106. let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
  1107. // Setting end dots
  1108. var duration = 0.0
  1109. do {
  1110. duration = try currentEntry?["duration"] as! Double
  1111. } catch {
  1112. print("No Duration Found")
  1113. }
  1114. // 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
  1115. if i > 0 {
  1116. let priorEntry = tempArray[i - 1] as [String : AnyObject]?
  1117. var priorBasalDate: String
  1118. if priorEntry?["timestamp"] != nil {
  1119. priorBasalDate = priorEntry?["timestamp"] as! String
  1120. } else if currentEntry?["created_at"] != nil {
  1121. priorBasalDate = priorEntry?["created_at"] as! String
  1122. } else {
  1123. continue
  1124. }
  1125. var priorStrippedZone = String(priorBasalDate.dropLast())
  1126. priorStrippedZone = priorStrippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
  1127. let priorDateFormatter = DateFormatter()
  1128. priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1129. priorDateFormatter.locale = Locale(identifier: "en_US")
  1130. priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1131. guard let priorDateString = dateFormatter.date(from: priorStrippedZone) else { continue }
  1132. let priorDateTimeStamp = priorDateString.timeIntervalSince1970
  1133. let priorDuration = priorEntry?["duration"] as! Double
  1134. // 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
  1135. if Double( dateTimeStamp - priorDateTimeStamp ) > Double( (priorDuration * 60) + 15 ) {
  1136. var scheduled = 0.0
  1137. var midGap = false
  1138. var midGapTime: TimeInterval = 0
  1139. var midGapValue: Double = 0
  1140. // cycle through basal profiles.
  1141. // TODO figure out how to deal with profile changes that happen mid-gap
  1142. for b in 0..<self.basalScheduleData.count {
  1143. if (priorDateTimeStamp + (priorDuration * 60)) >= basalScheduleData[b].date {
  1144. scheduled = basalScheduleData[b].basalRate
  1145. // deal with mid-gap scheduled basal change
  1146. // don't do it on the last scheudled basal entry
  1147. if b < self.basalScheduleData.count - 1 {
  1148. if dateTimeStamp > self.basalScheduleData[b + 1].date {
  1149. // midGap = true
  1150. // TODO: finish this to handle mid-gap items without crashing from overlapping entries
  1151. midGapTime = self.basalScheduleData[b + 1].date
  1152. midGapValue = self.basalScheduleData[b + 1].basalRate
  1153. }
  1154. }
  1155. }
  1156. }
  1157. // Make the starting dot at the last ending dot
  1158. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
  1159. basalData.append(startDot)
  1160. if midGap {
  1161. // Make the ending dot at the new scheduled basal
  1162. let endDot1 = basalGraphStruct(basalRate: scheduled, date: Double(midGapTime))
  1163. basalData.append(endDot1)
  1164. // Make the starting dot at the scheduled Time
  1165. let startDot2 = basalGraphStruct(basalRate: midGapValue, date: Double(midGapTime))
  1166. basalData.append(startDot2)
  1167. // Make the ending dot at the new basal value
  1168. let endDot2 = basalGraphStruct(basalRate: midGapValue, date: Double(dateTimeStamp))
  1169. basalData.append(endDot2)
  1170. } else {
  1171. // Make the ending dot at the new starting dot
  1172. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
  1173. basalData.append(endDot)
  1174. }
  1175. }
  1176. }
  1177. // Make the starting dot
  1178. let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))
  1179. basalData.append(startDot)
  1180. // Make the ending dot
  1181. // If it's the last one and has no duration, extend it for 30 minutes past the start. Otherwise set ending at duration
  1182. // duration is already set to 0 if there is no duration set on it.
  1183. //if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
  1184. if i == tempArray.count - 1 && duration == 0.0 {
  1185. lastEndDot = dateTimeStamp + (30 * 60)
  1186. latestBasal = String(format:"%.2f", basalRate)
  1187. } else {
  1188. lastEndDot = dateTimeStamp + (duration * 60)
  1189. latestBasal = String(format:"%.2f", basalRate)
  1190. }
  1191. // Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy
  1192. if i < tempArray.count - 1 {
  1193. let nextEntry = tempArray[i + 1] as [String : AnyObject]?
  1194. var nextBasalDate: String
  1195. if nextEntry?["timestamp"] != nil {
  1196. nextBasalDate = nextEntry?["timestamp"] as! String
  1197. } else if currentEntry?["created_at"] != nil {
  1198. nextBasalDate = nextEntry?["created_at"] as! String
  1199. } else {
  1200. continue
  1201. }
  1202. var nextStrippedZone = String(nextBasalDate.dropLast())
  1203. nextStrippedZone = nextStrippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
  1204. let nextDateFormatter = DateFormatter()
  1205. nextDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1206. nextDateFormatter.locale = Locale(identifier: "en_US")
  1207. nextDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1208. guard let nextDateString = dateFormatter.date(from: nextStrippedZone) else { continue }
  1209. let nextDateTimeStamp = nextDateString.timeIntervalSince1970
  1210. if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) {
  1211. lastEndDot = nextDateTimeStamp
  1212. }
  1213. }
  1214. let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))
  1215. basalData.append(endDot)
  1216. }
  1217. // If last basal was prior to right now, we need to create one last scheduled entry
  1218. if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() {
  1219. var scheduled = 0.0
  1220. // cycle through basal profiles.
  1221. // TODO figure out how to deal with profile changes that happen mid-gap
  1222. for b in 0..<self.basalProfile.count {
  1223. let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
  1224. let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
  1225. // check the prior temp ending to the profile seconds from midnight
  1226. print("yesterday " + String(scheduleTimeYesterday))
  1227. print("today " + String(scheduleTimeToday))
  1228. if lastEndDot >= scheduleTimeToday {
  1229. scheduled = basalProfile[b].value
  1230. }
  1231. }
  1232. latestBasal = String(format:"%.2f", scheduled)
  1233. // Make the starting dot at the last ending dot
  1234. let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))
  1235. basalData.append(startDot)
  1236. // Make the ending dot 10 minutes after now
  1237. let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970 + (60 * 10)))
  1238. basalData.append(endDot)
  1239. }
  1240. tableData[2].value = latestBasal
  1241. infoTable.reloadData()
  1242. if UserDefaultsRepository.graphBasal.value {
  1243. updateBasalGraph()
  1244. }
  1245. infoTable.reloadData()
  1246. }
  1247. // NS Meal Bolus Response Processor
  1248. func processNSBolus(entries: [[String:AnyObject]]) {
  1249. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Bolus") }
  1250. // because it's a small array, we're going to destroy and reload every time.
  1251. bolusData.removeAll()
  1252. var lastFoundIndex = 0
  1253. for i in 0..<entries.count {
  1254. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1255. var bolusDate: String
  1256. if currentEntry?["timestamp"] != nil {
  1257. bolusDate = currentEntry?["timestamp"] as! String
  1258. } else if currentEntry?["created_at"] != nil {
  1259. bolusDate = currentEntry?["created_at"] as! String
  1260. } else {
  1261. continue
  1262. }
  1263. // fix to remove millisecond (after period in timestamp) for FreeAPS users
  1264. var strippedZone = String(bolusDate.dropLast())
  1265. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1266. let dateFormatter = DateFormatter()
  1267. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1268. dateFormatter.locale = Locale(identifier: "en_US")
  1269. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1270. guard let dateString = dateFormatter.date(from: strippedZone) else { continue }
  1271. let dateTimeStamp = dateString.timeIntervalSince1970
  1272. guard let bolus = currentEntry?["insulin"] as? Double else { continue }
  1273. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1274. lastFoundIndex = sgv.foundIndex
  1275. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1276. // Make the dot
  1277. let dot = bolusGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv + 20))
  1278. bolusData.append(dot)
  1279. }
  1280. }
  1281. if UserDefaultsRepository.graphBolus.value {
  1282. updateBolusGraph()
  1283. }
  1284. }
  1285. // NS Carb Bolus Response Processor
  1286. func processNSCarbs(entries: [[String:AnyObject]]) {
  1287. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Carbs") }
  1288. // because it's a small array, we're going to destroy and reload every time.
  1289. carbData.removeAll()
  1290. var lastFoundIndex = 0
  1291. var lastFoundBolus = 0
  1292. for i in 0..<entries.count {
  1293. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1294. var carbDate: String
  1295. if currentEntry?["timestamp"] != nil {
  1296. carbDate = currentEntry?["timestamp"] as! String
  1297. } else if currentEntry?["created_at"] != nil {
  1298. carbDate = currentEntry?["created_at"] as! String
  1299. } else {
  1300. continue
  1301. }
  1302. let absorptionTime = currentEntry?["absorptionTime"] as? Int ?? 0
  1303. // Fix for FreeAPS milliseconds in timestamp
  1304. var strippedZone = String(carbDate.dropLast())
  1305. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1306. let dateFormatter = DateFormatter()
  1307. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1308. dateFormatter.locale = Locale(identifier: "en_US")
  1309. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1310. guard let dateString = dateFormatter.date(from: strippedZone) else { continue }
  1311. var dateTimeStamp = dateString.timeIntervalSince1970
  1312. guard let carbs = currentEntry?["carbs"] as? Double else {
  1313. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Carb entry")}
  1314. continue
  1315. }
  1316. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1317. lastFoundIndex = sgv.foundIndex
  1318. var offset = -50
  1319. if sgv.sgv < Double(topBG - 100) {
  1320. let bolusTime = findNearestBolusbyTime(timeWithin: 300, needle: dateTimeStamp, haystack: bolusData, startingIndex: lastFoundBolus)
  1321. lastFoundBolus = bolusTime.foundIndex
  1322. if bolusTime.offset {
  1323. offset = 70
  1324. } else {
  1325. offset = 20
  1326. }
  1327. }
  1328. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1329. // Make the dot
  1330. let dot = carbGraphStruct(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv + Double(offset)), absorptionTime: absorptionTime)
  1331. carbData.append(dot)
  1332. }
  1333. }
  1334. if UserDefaultsRepository.graphCarbs.value {
  1335. updateCarbGraph()
  1336. }
  1337. }
  1338. // NS Suspend Pump Response Processor
  1339. func processSuspendPump(entries: [[String:AnyObject]]) {
  1340. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Suspend Pump") }
  1341. // because it's a small array, we're going to destroy and reload every time.
  1342. suspendGraphData.removeAll()
  1343. var lastFoundIndex = 0
  1344. for i in 0..<entries.count {
  1345. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1346. var date: String
  1347. if currentEntry?["timestamp"] != nil {
  1348. date = currentEntry?["timestamp"] as! String
  1349. } else if currentEntry?["created_at"] != nil {
  1350. date = currentEntry?["created_at"] as! String
  1351. } else {
  1352. return
  1353. }
  1354. // Fix for FreeAPS milliseconds in timestamp
  1355. var strippedZone = String(date.dropLast())
  1356. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1357. let dateFormatter = DateFormatter()
  1358. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1359. dateFormatter.locale = Locale(identifier: "en_US")
  1360. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1361. let dateString = dateFormatter.date(from: strippedZone)
  1362. let dateTimeStamp = dateString!.timeIntervalSince1970
  1363. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1364. lastFoundIndex = sgv.foundIndex
  1365. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1366. // Make the dot
  1367. let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1368. suspendGraphData.append(dot)
  1369. }
  1370. }
  1371. if UserDefaultsRepository.graphOtherTreatments.value {
  1372. updateSuspendGraph()
  1373. }
  1374. }
  1375. // NS Resume Pump Response Processor
  1376. func processResumePump(entries: [[String:AnyObject]]) {
  1377. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Resume Pump") }
  1378. // because it's a small array, we're going to destroy and reload every time.
  1379. resumeGraphData.removeAll()
  1380. var lastFoundIndex = 0
  1381. for i in 0..<entries.count {
  1382. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1383. var date: String
  1384. if currentEntry?["timestamp"] != nil {
  1385. date = currentEntry?["timestamp"] as! String
  1386. } else if currentEntry?["created_at"] != nil {
  1387. date = currentEntry?["created_at"] as! String
  1388. } else {
  1389. return
  1390. }
  1391. // Fix for FreeAPS milliseconds in timestamp
  1392. var strippedZone = String(date.dropLast())
  1393. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1394. let dateFormatter = DateFormatter()
  1395. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1396. dateFormatter.locale = Locale(identifier: "en_US")
  1397. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1398. let dateString = dateFormatter.date(from: strippedZone)
  1399. let dateTimeStamp = dateString!.timeIntervalSince1970
  1400. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1401. lastFoundIndex = sgv.foundIndex
  1402. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1403. // Make the dot
  1404. let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1405. resumeGraphData.append(dot)
  1406. }
  1407. }
  1408. if UserDefaultsRepository.graphOtherTreatments.value {
  1409. updateResumeGraph()
  1410. }
  1411. }
  1412. // NS Sensor Start Response Processor
  1413. func processSensorStart(entries: [[String:AnyObject]]) {
  1414. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Sensor Start") }
  1415. // because it's a small array, we're going to destroy and reload every time.
  1416. sensorStartGraphData.removeAll()
  1417. var lastFoundIndex = 0
  1418. for i in 0..<entries.count {
  1419. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1420. var date: String
  1421. if currentEntry?["timestamp"] != nil {
  1422. date = currentEntry?["timestamp"] as! String
  1423. } else if currentEntry?["created_at"] != nil {
  1424. date = currentEntry?["created_at"] as! String
  1425. } else {
  1426. return
  1427. }
  1428. // Fix for FreeAPS milliseconds in timestamp
  1429. var strippedZone = String(date.dropLast())
  1430. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1431. let dateFormatter = DateFormatter()
  1432. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1433. dateFormatter.locale = Locale(identifier: "en_US")
  1434. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1435. let dateString = dateFormatter.date(from: strippedZone)
  1436. let dateTimeStamp = dateString!.timeIntervalSince1970
  1437. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1438. lastFoundIndex = sgv.foundIndex
  1439. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1440. // Make the dot
  1441. let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1442. sensorStartGraphData.append(dot)
  1443. }
  1444. }
  1445. if UserDefaultsRepository.graphOtherTreatments.value {
  1446. updateSensorStart()
  1447. }
  1448. }
  1449. // NS Note Response Processor
  1450. func processNotes(entries: [[String:AnyObject]]) {
  1451. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Notes") }
  1452. // because it's a small array, we're going to destroy and reload every time.
  1453. noteGraphData.removeAll()
  1454. var lastFoundIndex = 0
  1455. for i in 0..<entries.count {
  1456. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1457. var date: String
  1458. if currentEntry?["timestamp"] != nil {
  1459. date = currentEntry?["timestamp"] as! String
  1460. } else if currentEntry?["created_at"] != nil {
  1461. date = currentEntry?["created_at"] as! String
  1462. } else {
  1463. return
  1464. }
  1465. // Fix for FreeAPS milliseconds in timestamp
  1466. var strippedZone = String(date.dropLast())
  1467. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1468. let dateFormatter = DateFormatter()
  1469. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1470. dateFormatter.locale = Locale(identifier: "en_US")
  1471. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1472. let dateString = dateFormatter.date(from: strippedZone)
  1473. let dateTimeStamp = dateString!.timeIntervalSince1970
  1474. let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex)
  1475. lastFoundIndex = sgv.foundIndex
  1476. guard let thisNote = currentEntry?["notes"] as? String else { continue }
  1477. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1478. // Make the dot
  1479. let dot = DataStructs.noteStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv), note: thisNote)
  1480. noteGraphData.append(dot)
  1481. }
  1482. }
  1483. if UserDefaultsRepository.graphOtherTreatments.value {
  1484. updateNotes()
  1485. }
  1486. }
  1487. // NS BG Check Response Processor
  1488. func processNSBGCheck(entries: [[String:AnyObject]]) {
  1489. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: BG Check") }
  1490. // because it's a small array, we're going to destroy and reload every time.
  1491. bgCheckData.removeAll()
  1492. for i in 0..<entries.count {
  1493. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1494. var date: String
  1495. if currentEntry?["timestamp"] != nil {
  1496. date = currentEntry?["timestamp"] as! String
  1497. } else if currentEntry?["created_at"] != nil {
  1498. date = currentEntry?["created_at"] as! String
  1499. } else {
  1500. return
  1501. }
  1502. // Fix for FreeAPS milliseconds in timestamp
  1503. var strippedZone = String(date.dropLast())
  1504. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1505. let dateFormatter = DateFormatter()
  1506. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1507. dateFormatter.locale = Locale(identifier: "en_US")
  1508. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1509. let dateString = dateFormatter.date(from: strippedZone)
  1510. let dateTimeStamp = dateString!.timeIntervalSince1970
  1511. guard let sgv = currentEntry?["glucose"] as? Int else {
  1512. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Non-Int Glucose entry")}
  1513. continue
  1514. }
  1515. if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) {
  1516. // Make the dot
  1517. //let dot = ShareGlucoseData(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv))
  1518. let dot = ShareGlucoseData(sgv: sgv, date: Double(dateTimeStamp), direction: "")
  1519. bgCheckData.append(dot)
  1520. }
  1521. }
  1522. if UserDefaultsRepository.graphOtherTreatments.value {
  1523. updateBGCheckGraph()
  1524. }
  1525. }
  1526. // NS Override Response Processor
  1527. func processNSOverrides(entries: [[String:AnyObject]]) {
  1528. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Overrides") }
  1529. // because it's a small array, we're going to destroy and reload every time.
  1530. overrideGraphData.removeAll()
  1531. for i in 0..<entries.count {
  1532. let currentEntry = entries[entries.count - 1 - i] as [String : AnyObject]?
  1533. var date: String
  1534. if currentEntry?["timestamp"] != nil {
  1535. date = currentEntry?["timestamp"] as! String
  1536. } else if currentEntry?["created_at"] != nil {
  1537. date = currentEntry?["created_at"] as! String
  1538. } else {
  1539. return
  1540. }
  1541. // Fix for FreeAPS milliseconds in timestamp
  1542. var strippedZone = String(date.dropLast())
  1543. strippedZone = strippedZone.components(separatedBy: ".")[0]
  1544. let dateFormatter = DateFormatter()
  1545. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
  1546. dateFormatter.locale = Locale(identifier: "en_US")
  1547. dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  1548. let dateString = dateFormatter.date(from: strippedZone)
  1549. var dateTimeStamp = dateString!.timeIntervalSince1970
  1550. let graphHours = 24 * UserDefaultsRepository.downloadDays.value
  1551. if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) {
  1552. dateTimeStamp = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours)
  1553. }
  1554. var multiplier: Double = 1.0
  1555. if currentEntry?["insulinNeedsScaleFactor"] != nil {
  1556. multiplier = currentEntry?["insulinNeedsScaleFactor"] as! Double
  1557. }
  1558. var duration: Double = 5.0
  1559. if let durationType = currentEntry?["durationType"] as? String {
  1560. duration = dateTimeUtils.getNowTimeIntervalUTC() - dateTimeStamp + (60 * 60)
  1561. } else {
  1562. duration = (currentEntry?["duration"] as? Double)!
  1563. duration = duration * 60
  1564. }
  1565. // Skip overrides that aren't 5 minutes long. This prevents overlapping that causes bars to not display.
  1566. if duration < 300 { continue }
  1567. guard let enteredBy = currentEntry?["enteredBy"] as? String else { continue }
  1568. guard let reason = currentEntry?["reason"] as? String else { continue }
  1569. var range: [Int] = []
  1570. if let ranges = currentEntry?["correctionRange"] as? [Int] {
  1571. if ranges.count == 2 {
  1572. guard let low = ranges[0] as? Int else { continue }
  1573. guard let high = ranges[1] as? Int else { continue }
  1574. range.append(low)
  1575. range.append(high)
  1576. }
  1577. }
  1578. let endDate = dateTimeStamp + (duration)
  1579. let dot = DataStructs.overrideStruct(insulNeedsScaleFactor: multiplier, date: dateTimeStamp, endDate: endDate, duration: duration, correctionRange: range, enteredBy: enteredBy, reason: reason, sgv: -20)
  1580. overrideGraphData.append(dot)
  1581. }
  1582. if UserDefaultsRepository.graphOtherTreatments.value {
  1583. updateOverrideGraph()
  1584. }
  1585. }
  1586. }