DeviceStatus.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. // LoopFollow
  2. // DeviceStatus.swift
  3. import Charts
  4. import Foundation
  5. import HealthKit
  6. import UIKit
  7. extension MainViewController {
  8. func webLoadNSDeviceStatus() {
  9. let parameters = ["count": "1"]
  10. NightscoutUtils.executeDynamicRequest(eventType: .deviceStatus, parameters: parameters) { result in
  11. switch result {
  12. case let .success(json):
  13. if let jsonDeviceStatus = json as? [[String: AnyObject]] {
  14. DispatchQueue.main.async {
  15. self.updateDeviceStatusDisplay(jsonDeviceStatus: jsonDeviceStatus)
  16. Storage.shared.lastLoopingChecked.value = Date()
  17. }
  18. } else {
  19. self.handleDeviceStatusError()
  20. }
  21. case .failure:
  22. self.handleDeviceStatusError()
  23. }
  24. }
  25. }
  26. private func handleDeviceStatusError() {
  27. LogManager.shared.log(category: .deviceStatus, message: "Device status fetch failed!", limitIdentifier: "Device status fetch failed!")
  28. DispatchQueue.main.async {
  29. Storage.shared.lastLoopingChecked.value = Date()
  30. TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(10))
  31. self.evaluateNotLooping()
  32. }
  33. }
  34. func evaluateNotLooping() {
  35. guard let statusStackView = LoopStatusLabel.superview as? UIStackView else { return }
  36. guard let lastLoopTime = Observable.shared.alertLastLoopTime.value, lastLoopTime > 0 else {
  37. return
  38. }
  39. let now = TimeInterval(Date().timeIntervalSince1970)
  40. let nonLoopingTimeThreshold: TimeInterval = 15 * 60
  41. if IsNightscoutEnabled(), (now - lastLoopTime) >= nonLoopingTimeThreshold, lastLoopTime > 0 {
  42. IsNotLooping = true
  43. Observable.shared.isNotLooping.value = true
  44. statusStackView.distribution = .fill
  45. PredictionLabel.isHidden = true
  46. LoopStatusLabel.frame = CGRect(x: 0, y: 0, width: statusStackView.frame.width, height: statusStackView.frame.height)
  47. LoopStatusLabel.textAlignment = .center
  48. LoopStatusLabel.text = "⚠️ Not Looping!"
  49. LoopStatusLabel.textColor = UIColor.systemYellow
  50. LoopStatusLabel.font = UIFont.boldSystemFont(ofSize: 18)
  51. #if !targetEnvironment(macCatalyst)
  52. LiveActivityManager.shared.refreshFromCurrentState(reason: "notLooping")
  53. #endif
  54. } else {
  55. IsNotLooping = false
  56. Observable.shared.isNotLooping.value = false
  57. statusStackView.distribution = .fillEqually
  58. PredictionLabel.isHidden = false
  59. LoopStatusLabel.textAlignment = .right
  60. LoopStatusLabel.font = UIFont.systemFont(ofSize: 17)
  61. switch Storage.shared.appearanceMode.value {
  62. case .dark:
  63. LoopStatusLabel.textColor = UIColor.white
  64. case .light:
  65. LoopStatusLabel.textColor = UIColor.black
  66. case .system:
  67. LoopStatusLabel.textColor = UIColor.label
  68. }
  69. #if !targetEnvironment(macCatalyst)
  70. LiveActivityManager.shared.refreshFromCurrentState(reason: "loopingResumed")
  71. #endif
  72. }
  73. }
  74. // NS Device Status Response Processor
  75. func updateDeviceStatusDisplay(jsonDeviceStatus: [[String: AnyObject]]) {
  76. let previousIOBText = Observable.shared.iobText.value
  77. infoManager.clearInfoData(types: [.iob, .cob, .battery, .pump, .pumpBattery, .target, .isf, .carbRatio, .updated, .recBolus, .tdd])
  78. // For Loop, clear the current override here - For Trio, it is handled using treatments
  79. if Storage.shared.device.value == "Loop" {
  80. infoManager.clearInfoData(types: [.override])
  81. }
  82. if jsonDeviceStatus.count == 0 {
  83. LogManager.shared.log(category: .deviceStatus, message: "Device status is empty")
  84. TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(5 * 60))
  85. return
  86. }
  87. // Process the current data first
  88. let lastDeviceStatus = jsonDeviceStatus[0] as [String: AnyObject]?
  89. // pump and uploader
  90. let formatter = ISO8601DateFormatter()
  91. formatter.formatOptions = [.withFullDate,
  92. .withTime,
  93. .withDashSeparatorInDate,
  94. .withColonSeparatorInTime]
  95. Observable.shared.previousAlertLastLoopTime.value = Observable.shared.alertLastLoopTime.value
  96. if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? {
  97. if let bolusIncrement = lastPumpRecord["bolusIncrement"] as? Double, bolusIncrement > 0 {
  98. Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: bolusIncrement)
  99. Storage.shared.bolusIncrementDetected.value = true
  100. } else if let model = lastPumpRecord["model"] as? String, model == "Dash" {
  101. Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: 0.05)
  102. Storage.shared.bolusIncrementDetected.value = true
  103. } else {
  104. Storage.shared.bolusIncrementDetected.value = false
  105. }
  106. if let clockString = lastPumpRecord["clock"] as? String,
  107. let lastPumpTime = formatter.date(from: clockString)?.timeIntervalSince1970
  108. {
  109. let storedTime = Observable.shared.alertLastLoopTime.value ?? 0
  110. if lastPumpTime > storedTime {
  111. Observable.shared.alertLastLoopTime.value = lastPumpTime
  112. Storage.shared.lastLoopTime.value = lastPumpTime
  113. }
  114. if let reservoirData = lastPumpRecord["reservoir"] as? Double {
  115. latestPumpVolume = reservoirData
  116. infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U")
  117. Storage.shared.lastPumpReservoirU.value = reservoirData
  118. } else {
  119. latestPumpVolume = 50.0
  120. infoManager.updateInfoData(type: .pump, value: "50+U")
  121. Storage.shared.lastPumpReservoirU.value = nil
  122. }
  123. }
  124. // Parse pump battery percentage
  125. if let pumpBatteryRecord = lastPumpRecord["battery"] as? [String: AnyObject],
  126. let pumpBatteryPercent = pumpBatteryRecord["percent"] as? Double
  127. {
  128. infoManager.updateInfoData(type: .pumpBattery, value: String(format: "%.0f", pumpBatteryPercent) + "%")
  129. Observable.shared.pumpBatteryLevel.value = pumpBatteryPercent
  130. }
  131. if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject],
  132. let upbat = uploader["battery"] as? Double
  133. {
  134. let batteryText: String
  135. if let isCharging = uploader["isCharging"] as? Bool, isCharging {
  136. batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%"
  137. } else {
  138. batteryText = String(format: "%.0f", upbat) + "%"
  139. }
  140. infoManager.updateInfoData(type: .battery, value: batteryText)
  141. Observable.shared.deviceBatteryLevel.value = upbat
  142. let timestamp = uploader["timestamp"] as? Date ?? Date()
  143. let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp)
  144. deviceBatteryData.append(currentBattery)
  145. // store only the last 30 battery readings
  146. if deviceBatteryData.count > 30 {
  147. deviceBatteryData.removeFirst()
  148. }
  149. }
  150. }
  151. // Loop - handle new data
  152. if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? {
  153. DeviceStatusLoop(formatter: formatter, lastLoopRecord: lastLoopRecord)
  154. var oText = ""
  155. currentOverride = 1.0
  156. if let lastOverride = lastDeviceStatus?["override"] as? [String: AnyObject],
  157. let isActive = lastOverride["active"] as? Bool, isActive
  158. {
  159. if let lastCorrection = lastOverride["currentCorrectionRange"] as? [String: AnyObject],
  160. let minValue = lastCorrection["minValue"] as? Double,
  161. let maxValue = lastCorrection["maxValue"] as? Double
  162. {
  163. if let multiplier = lastOverride["multiplier"] as? Double {
  164. currentOverride = multiplier
  165. oText += String(format: "%.0f%%", multiplier * 100)
  166. } else {
  167. oText += "100%"
  168. }
  169. oText += " ("
  170. oText += Localizer.toDisplayUnits(String(minValue)) + "-" + Localizer.toDisplayUnits(String(maxValue)) + ")"
  171. }
  172. infoManager.updateInfoData(type: .override, value: oText)
  173. } else {
  174. infoManager.clearInfoData(type: .override)
  175. }
  176. }
  177. // OpenAPS - handle new data
  178. if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String: AnyObject]? {
  179. DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord)
  180. }
  181. // Start the timer based on the timestamp
  182. let now = dateTimeUtils.getNowTimeIntervalUTC()
  183. let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0)
  184. DispatchQueue.main.async {
  185. if secondsAgo >= (20 * 60) {
  186. TaskScheduler.shared.rescheduleTask(
  187. id: .deviceStatus,
  188. to: Date().addingTimeInterval(5 * 60)
  189. )
  190. } else if secondsAgo >= (10 * 60) {
  191. TaskScheduler.shared.rescheduleTask(
  192. id: .deviceStatus,
  193. to: Date().addingTimeInterval(60)
  194. )
  195. } else if secondsAgo >= (7 * 60) {
  196. TaskScheduler.shared.rescheduleTask(
  197. id: .deviceStatus,
  198. to: Date().addingTimeInterval(30)
  199. )
  200. } else if secondsAgo >= (5 * 60) {
  201. TaskScheduler.shared.rescheduleTask(
  202. id: .deviceStatus,
  203. to: Date().addingTimeInterval(10)
  204. )
  205. } else {
  206. let interval = (310 - secondsAgo)
  207. TaskScheduler.shared.rescheduleTask(
  208. id: .deviceStatus,
  209. to: Date().addingTimeInterval(interval)
  210. )
  211. TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3))
  212. }
  213. }
  214. evaluateNotLooping()
  215. // Mark device status as loaded for initial loading state
  216. markDataLoaded("deviceStatus")
  217. if Storage.shared.contactEnabled.value, Storage.shared.contactIOB.value != .off,
  218. Observable.shared.iobText.value != previousIOBText
  219. {
  220. contactImageUpdater.updateContactImage(
  221. bgValue: Observable.shared.bgText.value,
  222. trend: Observable.shared.directionText.value,
  223. delta: Observable.shared.deltaText.value,
  224. iob: Observable.shared.iobText.value,
  225. stale: Observable.shared.bgStale.value
  226. )
  227. }
  228. LogManager.shared.log(category: .deviceStatus, message: "Update Device Status done", isDebug: true)
  229. }
  230. }