NightScout.swift 92 KB

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