DeviceStatus.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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. func evaluateNotLooping(lastLoopTime: TimeInterval) {
  45. if let statusStackView = LoopStatusLabel.superview as? UIStackView {
  46. if ((TimeInterval(Date().timeIntervalSince1970) - lastLoopTime) / 60) > 15 {
  47. IsNotLooping = true
  48. // Change the distribution to 'fill' to allow manual resizing of arranged subviews
  49. statusStackView.distribution = .fill
  50. // Hide PredictionLabel and expand LoopStatusLabel to fill the entire stack view
  51. PredictionLabel.isHidden = true
  52. LoopStatusLabel.frame = CGRect(x: 0, y: 0, width: statusStackView.frame.width, height: statusStackView.frame.height)
  53. // Update LoopStatusLabel's properties to display Not Looping
  54. LoopStatusLabel.textAlignment = .center
  55. LoopStatusLabel.text = "⚠️ Not Looping!"
  56. LoopStatusLabel.textColor = UIColor.systemYellow
  57. LoopStatusLabel.font = UIFont.boldSystemFont(ofSize: 18)
  58. } else {
  59. IsNotLooping = false
  60. // Restore the original distribution and visibility of labels
  61. statusStackView.distribution = .fillEqually
  62. PredictionLabel.isHidden = false
  63. // Reset LoopStatusLabel's properties
  64. LoopStatusLabel.textAlignment = .right
  65. LoopStatusLabel.font = UIFont.systemFont(ofSize: 17)
  66. if UserDefaultsRepository.forceDarkMode.value {
  67. LoopStatusLabel.textColor = UIColor.white
  68. } else {
  69. LoopStatusLabel.textColor = UIColor.black
  70. }
  71. }
  72. }
  73. latestLoopTime = lastLoopTime
  74. }
  75. // NS Device Status Response Processor
  76. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
  77. self.clearLastInfoData(index: 0)
  78. self.clearLastInfoData(index: 1)
  79. self.clearLastInfoData(index: 3)
  80. self.clearLastInfoData(index: 4)
  81. self.clearLastInfoData(index: 5)
  82. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
  83. if jsonDeviceStatus.count == 0 {
  84. return
  85. }
  86. //Process the current data first
  87. let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]?
  88. //pump and uploader
  89. let formatter = ISO8601DateFormatter()
  90. formatter.formatOptions = [.withFullDate,
  91. .withTime,
  92. .withDashSeparatorInDate,
  93. .withColonSeparatorInTime]
  94. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? {
  95. if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 {
  96. if let reservoirData = lastPumpRecord["reservoir"] as? Double {
  97. latestPumpVolume = reservoirData
  98. tableData[5].value = String(format:"%.0f", reservoirData) + "U"
  99. } else {
  100. latestPumpVolume = 50.0
  101. tableData[5].value = "50+U"
  102. }
  103. if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] {
  104. let upbat = uploader["battery"] as! Double
  105. tableData[4].value = String(format:"%.0f", upbat) + "%"
  106. UserDefaultsRepository.deviceBatteryLevel.value = upbat
  107. }
  108. }
  109. }
  110. // Loop
  111. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? {
  112. //print("Loop: \(lastLoopRecord)")
  113. if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 {
  114. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  115. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  116. if let failure = lastLoopRecord["failureReason"] {
  117. LoopStatusLabel.text = "X"
  118. latestLoopStatusString = "X"
  119. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  120. } else {
  121. var wasEnacted = false
  122. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  123. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  124. wasEnacted = true
  125. if let lastTempBasal = enacted["rate"] as? Double {
  126. }
  127. }
  128. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  129. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  130. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  131. }
  132. if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] {
  133. tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double)
  134. latestCOB = String(format:"%.0f", cobdata["cob"] as! Double)
  135. }
  136. if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] {
  137. let prediction = predictdata["values"] as! [Double]
  138. PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!)))
  139. PredictionLabel.textColor = UIColor.systemPurple
  140. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  141. predictionData.removeAll()
  142. var predictionTime = lastLoopTime
  143. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  144. var i = 0
  145. while i <= toLoad {
  146. if i < prediction.count {
  147. let sgvValue = Int(round(prediction[i]))
  148. // Skip values higher than 600
  149. if sgvValue <= 600 {
  150. let prediction = ShareGlucoseData(sgv: sgvValue, date: predictionTime, direction: "flat")
  151. predictionData.append(prediction)
  152. }
  153. predictionTime += 300
  154. }
  155. i += 1
  156. }
  157. let predMin = prediction.min()
  158. let predMax = prediction.max()
  159. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  160. updatePredictionGraph()
  161. }
  162. }
  163. if let recBolus = lastLoopRecord["recommendedBolus"] as? Double {
  164. tableData[8].value = String(format:"%.2fU", recBolus)
  165. }
  166. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  167. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  168. var lastBGTime = lastLoopTime
  169. if bgData.count > 0 {
  170. lastBGTime = bgData[bgData.count - 1].date
  171. }
  172. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  173. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  174. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  175. if tempBasalTime > lastBGTime && !wasEnacted {
  176. LoopStatusLabel.text = "⏀"
  177. latestLoopStatusString = "⏀"
  178. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  179. } else {
  180. LoopStatusLabel.text = "↻"
  181. latestLoopStatusString = "↻"
  182. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  183. }
  184. }
  185. } else {
  186. LoopStatusLabel.text = "↻"
  187. latestLoopStatusString = "↻"
  188. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  189. }
  190. }
  191. evaluateNotLooping(lastLoopTime: lastLoopTime)
  192. } // end lastLoopTime
  193. } // end lastLoop Record
  194. if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String : AnyObject]? {
  195. if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970 {
  196. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  197. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  198. if let failure = lastLoopRecord["failureReason"] {
  199. LoopStatusLabel.text = "X"
  200. latestLoopStatusString = "X"
  201. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  202. } else {
  203. var wasEnacted = false
  204. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  205. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  206. wasEnacted = true
  207. if let lastTempBasal = enacted["rate"] as? Double {
  208. }
  209. }
  210. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  211. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  212. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  213. }
  214. if let cobdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  215. tableData[1].value = String(format:"%.0f", cobdata["COB"] as! Double)
  216. latestCOB = String(format:"%.0f", cobdata["COB"] as! Double)
  217. }
  218. if let recbolusdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  219. tableData[8].value = String(format:"%.2fU", recbolusdata["insulinReq"] as! Double)
  220. }
  221. if let autosensdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  222. let sens = autosensdata["sensitivityRatio"] as! Double * 100.0
  223. tableData[11].value = String(format:"%.0f", sens) + "%"
  224. }
  225. //Picks COB prediction if available, else UAM, else IOB, else ZT
  226. //Ideal is to predict all 4 in Loop Follow but this is a quick start
  227. var graphtype = ""
  228. var predictioncolor = UIColor.systemGray
  229. PredictionLabel.textColor = predictioncolor
  230. if let enactdata = lastLoopRecord["enacted"] as? [String:AnyObject],
  231. let predbgdata = enactdata["predBGs"] as? [String:AnyObject] {
  232. if predbgdata["COB"] != nil {
  233. graphtype = "COB"
  234. } else if predbgdata["UAM"] != nil {
  235. graphtype = "UAM"
  236. } else if predbgdata["IOB"] != nil {
  237. graphtype = "IOB"
  238. } else {
  239. graphtype = "ZT"
  240. }
  241. // Access the color based on graphtype
  242. var colorName = ""
  243. switch graphtype {
  244. case "COB": colorName = "LoopYellow"
  245. case "UAM": colorName = "UAM"
  246. case "IOB": colorName = "Insulin"
  247. case "ZT": colorName = "ZT"
  248. default: break
  249. }
  250. if let selectedColor = UIColor(named: colorName) {
  251. predictioncolor = selectedColor
  252. PredictionLabel.textColor = predictioncolor
  253. }
  254. let graphdata = predbgdata[graphtype] as! [Double]
  255. if let eventualdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  256. if let eventualBGValue = eventualdata["eventualBG"] as? NSNumber {
  257. let eventualBGStringValue = String(describing: eventualBGValue)
  258. PredictionLabel.text = bgUnits.toDisplayUnits(eventualBGStringValue)
  259. }
  260. }
  261. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  262. predictionData.removeAll()
  263. var predictionTime = lastLoopTime
  264. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  265. var i = 0
  266. while i <= toLoad {
  267. if i < graphdata.count {
  268. let prediction = ShareGlucoseData(sgv: Int(round(graphdata[i])), date: predictionTime, direction: "flat")
  269. predictionData.append(prediction)
  270. predictionTime += 300
  271. }
  272. i += 1
  273. }
  274. }
  275. let predMin = graphdata.min()
  276. let predMax = graphdata.max()
  277. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  278. updatePredictionGraph(color: predictioncolor)
  279. }
  280. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  281. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  282. var lastBGTime = lastLoopTime
  283. if bgData.count > 0 {
  284. lastBGTime = bgData[bgData.count - 1].date
  285. }
  286. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  287. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  288. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  289. if tempBasalTime > lastBGTime && !wasEnacted {
  290. LoopStatusLabel.text = "⏀"
  291. latestLoopStatusString = "⏀"
  292. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  293. } else {
  294. LoopStatusLabel.text = "↻"
  295. latestLoopStatusString = "↻"
  296. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  297. }
  298. }
  299. } else {
  300. LoopStatusLabel.text = "↻"
  301. latestLoopStatusString = "↻"
  302. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  303. }
  304. }
  305. evaluateNotLooping(lastLoopTime: lastLoopTime)
  306. }
  307. }
  308. var oText = "" as String
  309. currentOverride = 1.0
  310. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  311. if lastOverride["active"] as! Bool {
  312. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  313. if let multiplier = lastOverride["multiplier"] as? Double {
  314. currentOverride = multiplier
  315. oText += String(format: "%.0f%%", (multiplier * 100))
  316. }
  317. else
  318. {
  319. oText += "100%"
  320. }
  321. oText += " ("
  322. let minValue = lastCorrection["minValue"] as! Double
  323. let maxValue = lastCorrection["maxValue"] as! Double
  324. oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
  325. tableData[3].value = oText
  326. }
  327. }
  328. infoTable.reloadData()
  329. // Start the timer based on the timestamp
  330. let now = dateTimeUtils.getNowTimeIntervalUTC()
  331. let secondsAgo = now - latestLoopTime
  332. DispatchQueue.main.async {
  333. // if Loop is overdue over: 20:00, re-attempt every 5 minutes
  334. if secondsAgo >= (20 * 60) {
  335. self.startDeviceStatusTimer(time: (5 * 60))
  336. print("started 5 minute device status timer")
  337. // if the Loop is overdue: 10:00-19:59, re-attempt every minute
  338. } else if secondsAgo >= (10 * 60) {
  339. self.startDeviceStatusTimer(time: 60)
  340. print("started 1 minute device status timer")
  341. // if the Loop is overdue: 7:00-9:59, re-attempt every 30 seconds
  342. } else if secondsAgo >= (7 * 60) {
  343. self.startDeviceStatusTimer(time: 30)
  344. print("started 30 second device status timer")
  345. // if the Loop is overdue: 5:00-6:59 re-attempt every 10 seconds
  346. } else if secondsAgo >= (5 * 60) {
  347. self.startDeviceStatusTimer(time: 10)
  348. print("started 10 second device status timer")
  349. // We have a current Loop. Set timer to 5:10 from last reading
  350. } else {
  351. self.startDeviceStatusTimer(time: 310 - secondsAgo)
  352. let timerVal = 310 - secondsAgo
  353. print("started 5:10 device status timer: \(timerVal)")
  354. }
  355. }
  356. }
  357. }