DeviceStatus.swift 20 KB

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