DeviceStatus.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. //
  2. // DeviceStatus.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2023-10-05.
  6. // Copyright © 2023 Jon Fawcett. All rights reserved.
  7. //
  8. import Foundation
  9. import UIKit
  10. extension MainViewController {
  11. // NS Device Status Web Call
  12. func webLoadNSDeviceStatus() {
  13. if UserDefaultsRepository.debugLog.value {
  14. self.writeDebugLog(value: "Download: device status")
  15. }
  16. let parameters: [String: String] = ["count": "288"]
  17. NightscoutUtils.executeDynamicRequest(eventType: .deviceStatus, parameters: parameters) { result in
  18. switch result {
  19. case .success(let json):
  20. if let jsonDeviceStatus = json as? [[String: AnyObject]] {
  21. DispatchQueue.main.async {
  22. self.updateDeviceStatusDisplay(jsonDeviceStatus: jsonDeviceStatus)
  23. }
  24. } else {
  25. self.handleDeviceStatusError()
  26. }
  27. case .failure:
  28. self.handleDeviceStatusError()
  29. }
  30. }
  31. }
  32. private func handleDeviceStatusError() {
  33. if globalVariables.nsVerifiedAlert < dateTimeUtils.getNowTimeIntervalUTC() + 300 {
  34. globalVariables.nsVerifiedAlert = dateTimeUtils.getNowTimeIntervalUTC()
  35. //self.sendNotification(title: "Nightscout Error", body: "Please double check url, token, and internet connection. This may also indicate a temporary Nightscout issue")
  36. }
  37. DispatchQueue.main.async {
  38. if self.deviceStatusTimer.isValid {
  39. self.deviceStatusTimer.invalidate()
  40. }
  41. self.startDeviceStatusTimer(time: 10)
  42. }
  43. }
  44. // NS Device Status Response Processor
  45. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  46. self.clearLastInfoData(index: 0)
  47. self.clearLastInfoData(index: 1)
  48. self.clearLastInfoData(index: 3)
  49. self.clearLastInfoData(index: 4)
  50. self.clearLastInfoData(index: 5)
  51. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
  52. if jsonDeviceStatus.count == 0 {
  53. return
  54. }
  55. //Process the current data first
  56. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  57. //pump and uploader
  58. let formatter = ISO8601DateFormatter()
  59. formatter.formatOptions = [.withFullDate,
  60. .withTime,
  61. .withDashSeparatorInDate,
  62. .withColonSeparatorInTime]
  63. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  64. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  65. if let reservoirData = lastPumpRecord["reservoir"] as? Double {
  66. latestPumpVolume = reservoirData
  67. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  68. } else {
  69. latestPumpVolume = 50.0
  70. tableData[5].value = "50+U"
  71. }
  72. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  73. let upbat = uploader["battery"] as! Double
  74. tableData[4].value = String(format:"%.0f", upbat) + "%"
  75. UserDefaultsRepository.deviceBatteryLevel.value = upbat
  76. }
  77. }
  78. }
  79. // Loop
  80. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  81. //print("Loop: \(lastLoopRecord)")
  82. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  83. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  84. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  85. if let failure = lastLoopRecord["failureReason"] {
  86. LoopStatusLabel.text = "X"
  87. latestLoopStatusString = "X"
  88. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  89. } else {
  90. var wasEnacted = false
  91. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  92. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  93. wasEnacted = true
  94. if let lastTempBasal = enacted["rate"] as? Double {
  95. }
  96. }
  97. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  98. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  99. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  100. }
  101. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  102. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  103. latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
  104. }
  105. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  106. let prediction = predictdata["values"] as! [Double]
  107. PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
  108. PredictionLabel.textColor = UIColor.systemPurple
  109. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  110. predictionData.removeAll()
  111. var predictionTime = lastLoopTime
  112. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  113. var i = 0
  114. while i <= toLoad {
  115. if i < prediction.count {
  116. let sgvValue = Int(round(prediction[i]))
  117. // Skip values higher than 600
  118. if sgvValue <= 600 {
  119. let prediction = ShareGlucoseData(sgv: sgvValue, date: predictionTime, direction: "flat")
  120. predictionData.append(prediction)
  121. }
  122. predictionTime += 300
  123. }
  124. i += 1
  125. }
  126. let predMin = prediction.min()
  127. let predMax = prediction.max()
  128. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  129. updatePredictionGraph()
  130. }
  131. }
  132. if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
  133. tableData[8].value = String(format:"%.2fU", recBolus)
  134. }
  135. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  136. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  137. var lastBGTime = lastLoopTime
  138. if bgData.count > 0 {
  139. lastBGTime = bgData[bgData.count - 1].date
  140. }
  141. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  142. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  143. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  144. if tempBasalTime > lastBGTime && !wasEnacted {
  145. LoopStatusLabel.text = "⏀"
  146. latestLoopStatusString = "⏀"
  147. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  148. } else {
  149. LoopStatusLabel.text = "↻"
  150. latestLoopStatusString = "↻"
  151. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  152. }
  153. }
  154. } else {
  155. LoopStatusLabel.text = "↻"
  156. latestLoopStatusString = "↻"
  157. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  158. }
  159. }
  160. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 {
  161. LoopStatusLabel.text = "⚠"
  162. latestLoopStatusString = "⚠"
  163. }
  164. latestLoopTime = lastLoopTime
  165. } // end lastLoopTime
  166. } // end lastLoop Record
  167. if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String : AnyObject]? {
  168. if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970 {
  169. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  170. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  171. if let failure = lastLoopRecord["failureReason"] {
  172. LoopStatusLabel.text = "X"
  173. latestLoopStatusString = "X"
  174. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  175. } else {
  176. var wasEnacted = false
  177. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  178. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  179. wasEnacted = true
  180. if let lastTempBasal = enacted["rate"] as? Double {
  181. }
  182. }
  183. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  184. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  185. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  186. }
  187. if let cobdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  188. tableData[1].value = String(format:"%.1f", cobdata["COB"] as! Double)
  189. latestCOB = String(format:"%.1f", cobdata["COB"] as! Double)
  190. }
  191. if let recbolusdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  192. tableData[8].value = String(format:"%.2fU", recbolusdata["insulinReq"] as! Double)
  193. }
  194. if let autosensdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  195. let sens = autosensdata["sensitivityRatio"] as! Double * 100.0
  196. tableData[11].value = String(format:"%.0f", sens) + "%"
  197. }
  198. //Picks COB prediction if available, else UAM, else IOB, else ZT
  199. //Ideal is to predict all 4 in Loop Follow but this is a quick start
  200. var graphtype = ""
  201. var predictioncolor = UIColor.systemGray
  202. PredictionLabel.textColor = predictioncolor
  203. if let enactdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  204. if let predbgdata = enactdata["predBGs"] as? [String:AnyObject] {
  205. if predbgdata["COB"] != nil {
  206. graphtype="COB"
  207. predictioncolor = UIColor.systemYellow
  208. PredictionLabel.textColor = predictioncolor
  209. }
  210. else if predbgdata["UAM"] != nil {
  211. graphtype="UAM"
  212. predictioncolor = UIColor.systemOrange
  213. PredictionLabel.textColor = predictioncolor
  214. }
  215. else if predbgdata["IOB"] != nil {
  216. graphtype="IOB"
  217. predictioncolor = UIColor.systemBlue
  218. PredictionLabel.textColor = predictioncolor
  219. }
  220. else {
  221. graphtype="ZT"
  222. predictioncolor = UIColor.systemGreen
  223. PredictionLabel.textColor = predictioncolor
  224. }
  225. let graphdata = predbgdata[graphtype] as! [Double]
  226. if let eventualdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  227. if let eventualBGValue = eventualdata["eventualBG"] as? NSNumber {
  228. let eventualBGStringValue = String(describing: eventualBGValue)
  229. PredictionLabel.text = bgUnits.toDisplayUnits(eventualBGStringValue)
  230. }
  231. }
  232. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  233. predictionData.removeAll()
  234. var predictionTime = lastLoopTime
  235. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  236. var i = 0
  237. while i <= toLoad {
  238. if i < graphdata.count {
  239. let prediction = ShareGlucoseData(sgv: Int(round(graphdata[i])), date: predictionTime, direction: "flat")
  240. predictionData.append(prediction)
  241. predictionTime += 300
  242. }
  243. i += 1
  244. }
  245. }
  246. let predMin = graphdata.min()
  247. let predMax = graphdata.max()
  248. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  249. updatePredictionGraph(color: predictioncolor)
  250. }
  251. }
  252. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  253. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  254. var lastBGTime = lastLoopTime
  255. if bgData.count > 0 {
  256. lastBGTime = bgData[bgData.count - 1].date
  257. }
  258. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  259. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  260. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  261. if tempBasalTime > lastBGTime && !wasEnacted {
  262. LoopStatusLabel.text = "⏀"
  263. latestLoopStatusString = "⏀"
  264. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  265. } else {
  266. LoopStatusLabel.text = "↻"
  267. latestLoopStatusString = "↻"
  268. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  269. }
  270. }
  271. } else {
  272. LoopStatusLabel.text = "↻"
  273. latestLoopStatusString = "↻"
  274. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  275. }
  276. }
  277. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 {
  278. LoopStatusLabel.text = "⚠"
  279. latestLoopStatusString = "⚠"
  280. }
  281. latestLoopTime = lastLoopTime
  282. }
  283. }
  284. var oText = "" as String
  285. currentOverride = 1.0
  286. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  287. if lastOverride["active"] as! Bool {
  288. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  289. if let multiplier = lastOverride["multiplier"] as? Double {
  290. currentOverride = multiplier
  291. oText += String(format: "%.0f%%", (multiplier * 100))
  292. }
  293. else
  294. {
  295. oText += "100%"
  296. }
  297. oText += " ("
  298. let minValue = lastCorrection["minValue"] as! Double
  299. let maxValue = lastCorrection["maxValue"] as! Double
  300. oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
  301. tableData[3].value = oText
  302. }
  303. }
  304. infoTable.reloadData()
  305. // Start the timer based on the timestamp
  306. let now = dateTimeUtils.getNowTimeIntervalUTC()
  307. let secondsAgo = now - latestLoopTime
  308. DispatchQueue.main.async {
  309. // if Loop is overdue over: 20:00, re-attempt every 5 minutes
  310. if secondsAgo >= (20 * 60) {
  311. self.startDeviceStatusTimer(time: (5 * 60))
  312. print("started 5 minute device status timer")
  313. // if the Loop is overdue: 10:00-19:59, re-attempt every minute
  314. } else if secondsAgo >= (10 * 60) {
  315. self.startDeviceStatusTimer(time: 60)
  316. print("started 1 minute device status timer")
  317. // if the Loop is overdue: 7:00-9:59, re-attempt every 30 seconds
  318. } else if secondsAgo >= (7 * 60) {
  319. self.startDeviceStatusTimer(time: 30)
  320. print("started 30 second device status timer")
  321. // if the Loop is overdue: 5:00-6:59 re-attempt every 10 seconds
  322. } else if secondsAgo >= (5 * 60) {
  323. self.startDeviceStatusTimer(time: 10)
  324. print("started 10 second device status timer")
  325. // We have a current Loop. Set timer to 5:10 from last reading
  326. } else {
  327. self.startDeviceStatusTimer(time: 310 - secondsAgo)
  328. let timerVal = 310 - secondsAgo
  329. print("started 5:10 device status timer: \(timerVal)")
  330. }
  331. }
  332. }
  333. }