DeviceStatus.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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. UserDefaultsRepository.deviceRecBolus.value = recBolus
  166. }
  167. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  168. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  169. var lastBGTime = lastLoopTime
  170. if bgData.count > 0 {
  171. lastBGTime = bgData[bgData.count - 1].date
  172. }
  173. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  174. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  175. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  176. if tempBasalTime > lastBGTime && !wasEnacted {
  177. LoopStatusLabel.text = "⏀"
  178. latestLoopStatusString = "⏀"
  179. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  180. } else {
  181. LoopStatusLabel.text = "↻"
  182. latestLoopStatusString = "↻"
  183. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  184. }
  185. }
  186. } else {
  187. LoopStatusLabel.text = "↻"
  188. latestLoopStatusString = "↻"
  189. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  190. }
  191. }
  192. evaluateNotLooping(lastLoopTime: lastLoopTime)
  193. } // end lastLoopTime
  194. } // end lastLoop Record
  195. if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String : AnyObject]? {
  196. if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970 {
  197. UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime
  198. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastLoopTime: " + String(lastLoopTime)) }
  199. if let failure = lastLoopRecord["failureReason"] {
  200. LoopStatusLabel.text = "X"
  201. latestLoopStatusString = "X"
  202. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") }
  203. } else {
  204. var wasEnacted = false
  205. if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] {
  206. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop: Was Enacted") }
  207. wasEnacted = true
  208. if let lastTempBasal = enacted["rate"] as? Double {
  209. }
  210. }
  211. if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] {
  212. tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double))
  213. latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double))
  214. }
  215. if let cobdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  216. tableData[1].value = String(format:"%.0f", cobdata["COB"] as! Double)
  217. latestCOB = String(format:"%.0f", cobdata["COB"] as! Double)
  218. }
  219. if let recbolusdata = lastLoopRecord["enacted"] as? [String: AnyObject],
  220. let insulinReq = recbolusdata["insulinReq"] as? Double {
  221. tableData[8].value = String(format: "%.2fU", insulinReq)
  222. UserDefaultsRepository.deviceRecBolus.value = insulinReq
  223. } else {
  224. tableData[8].value = "N/A"
  225. UserDefaultsRepository.deviceRecBolus.value = 0
  226. print("Warning: Failed to extract insulinReq from recbolusdata.")
  227. }
  228. if let autosensdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  229. let sens = autosensdata["sensitivityRatio"] as! Double * 100.0
  230. tableData[11].value = String(format:"%.0f", sens) + "%"
  231. }
  232. //Picks COB prediction if available, else UAM, else IOB, else ZT
  233. //Ideal is to predict all 4 in Loop Follow but this is a quick start
  234. var graphtype = ""
  235. var predictioncolor = UIColor.systemGray
  236. PredictionLabel.textColor = predictioncolor
  237. if let enactdata = lastLoopRecord["enacted"] as? [String:AnyObject],
  238. let predbgdata = enactdata["predBGs"] as? [String:AnyObject] {
  239. if predbgdata["COB"] != nil {
  240. graphtype = "COB"
  241. } else if predbgdata["UAM"] != nil {
  242. graphtype = "UAM"
  243. } else if predbgdata["IOB"] != nil {
  244. graphtype = "IOB"
  245. } else {
  246. graphtype = "ZT"
  247. }
  248. // Access the color based on graphtype
  249. var colorName = ""
  250. switch graphtype {
  251. case "COB": colorName = "LoopYellow"
  252. case "UAM": colorName = "UAM"
  253. case "IOB": colorName = "Insulin"
  254. case "ZT": colorName = "ZT"
  255. default: break
  256. }
  257. if let selectedColor = UIColor(named: colorName) {
  258. predictioncolor = selectedColor
  259. PredictionLabel.textColor = predictioncolor
  260. }
  261. let graphdata = predbgdata[graphtype] as! [Double]
  262. if let eventualdata = lastLoopRecord["enacted"] as? [String:AnyObject] {
  263. if let eventualBGValue = eventualdata["eventualBG"] as? NSNumber {
  264. let eventualBGStringValue = String(describing: eventualBGValue)
  265. PredictionLabel.text = bgUnits.toDisplayUnits(eventualBGStringValue)
  266. }
  267. }
  268. if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime {
  269. predictionData.removeAll()
  270. var predictionTime = lastLoopTime
  271. let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12)
  272. var i = 0
  273. while i <= toLoad {
  274. if i < graphdata.count {
  275. let prediction = ShareGlucoseData(sgv: Int(round(graphdata[i])), date: predictionTime, direction: "flat")
  276. predictionData.append(prediction)
  277. predictionTime += 300
  278. }
  279. i += 1
  280. }
  281. }
  282. let predMin = graphdata.min()
  283. let predMax = graphdata.max()
  284. tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!))
  285. updatePredictionGraph(color: predictioncolor)
  286. }
  287. if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] {
  288. if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 {
  289. var lastBGTime = lastLoopTime
  290. if bgData.count > 0 {
  291. lastBGTime = bgData[bgData.count - 1].date
  292. }
  293. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "tempBasalTime: " + String(tempBasalTime)) }
  294. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "lastBGTime: " + String(lastBGTime)) }
  295. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "wasEnacted: " + String(wasEnacted)) }
  296. if tempBasalTime > lastBGTime && !wasEnacted {
  297. LoopStatusLabel.text = "⏀"
  298. latestLoopStatusString = "⏀"
  299. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Open Loop: recommended temp. temp time > bg time, was not enacted") }
  300. } else {
  301. LoopStatusLabel.text = "↻"
  302. latestLoopStatusString = "↻"
  303. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: recommended temp, but temp time is < bg time and/or was enacted") }
  304. }
  305. }
  306. } else {
  307. LoopStatusLabel.text = "↻"
  308. latestLoopStatusString = "↻"
  309. if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Looping: no recommended temp") }
  310. }
  311. }
  312. evaluateNotLooping(lastLoopTime: lastLoopTime)
  313. }
  314. }
  315. var oText = "" as String
  316. currentOverride = 1.0
  317. if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? {
  318. if lastOverride["active"] as! Bool {
  319. let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject]
  320. if let multiplier = lastOverride["multiplier"] as? Double {
  321. currentOverride = multiplier
  322. oText += String(format: "%.0f%%", (multiplier * 100))
  323. }
  324. else
  325. {
  326. oText += "100%"
  327. }
  328. oText += " ("
  329. let minValue = lastCorrection["minValue"] as! Double
  330. let maxValue = lastCorrection["maxValue"] as! Double
  331. oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
  332. tableData[3].value = oText
  333. }
  334. }
  335. infoTable.reloadData()
  336. // Start the timer based on the timestamp
  337. let now = dateTimeUtils.getNowTimeIntervalUTC()
  338. let secondsAgo = now - latestLoopTime
  339. DispatchQueue.main.async {
  340. // if Loop is overdue over: 20:00, re-attempt every 5 minutes
  341. if secondsAgo >= (20 * 60) {
  342. self.startDeviceStatusTimer(time: (5 * 60))
  343. print("started 5 minute device status timer")
  344. // if the Loop is overdue: 10:00-19:59, re-attempt every minute
  345. } else if secondsAgo >= (10 * 60) {
  346. self.startDeviceStatusTimer(time: 60)
  347. print("started 1 minute device status timer")
  348. // if the Loop is overdue: 7:00-9:59, re-attempt every 30 seconds
  349. } else if secondsAgo >= (7 * 60) {
  350. self.startDeviceStatusTimer(time: 30)
  351. print("started 30 second device status timer")
  352. // if the Loop is overdue: 5:00-6:59 re-attempt every 10 seconds
  353. } else if secondsAgo >= (5 * 60) {
  354. self.startDeviceStatusTimer(time: 10)
  355. print("started 10 second device status timer")
  356. // We have a current Loop. Set timer to 5:10 from last reading
  357. } else {
  358. self.startDeviceStatusTimer(time: 310 - secondsAgo)
  359. let timerVal = 310 - secondsAgo
  360. print("started 5:10 device status timer: \(timerVal)")
  361. }
  362. }
  363. }
  364. }