| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- // LoopFollow
- // DeviceStatus.swift
- import Foundation
- import HealthKit
- import SwiftUI
- extension MainViewController {
- func webLoadNSDeviceStatus() {
- let parameters = ["count": "1"]
- NightscoutUtils.executeDynamicRequest(eventType: .deviceStatus, parameters: parameters) { result in
- switch result {
- case let .success(json):
- if let jsonDeviceStatus = json as? [[String: AnyObject]] {
- DispatchQueue.main.async {
- self.updateDeviceStatusDisplay(jsonDeviceStatus: jsonDeviceStatus)
- Storage.shared.lastLoopingChecked.value = Date()
- }
- } else {
- self.handleDeviceStatusError()
- }
- case .failure:
- self.handleDeviceStatusError()
- }
- }
- }
- private func handleDeviceStatusError() {
- LogManager.shared.log(category: .deviceStatus, message: "Device status fetch failed!", limitIdentifier: "Device status fetch failed!")
- DispatchQueue.main.async {
- Storage.shared.lastLoopingChecked.value = Date()
- TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(10))
- self.evaluateNotLooping()
- }
- }
- func evaluateNotLooping() {
- guard let lastLoopTime = Observable.shared.alertLastLoopTime.value, lastLoopTime > 0 else {
- return
- }
- let now = TimeInterval(Date().timeIntervalSince1970)
- let nonLoopingTimeThreshold: TimeInterval = 15 * 60
- if IsNightscoutEnabled(), (now - lastLoopTime) >= nonLoopingTimeThreshold, lastLoopTime > 0 {
- IsNotLooping = true
- Observable.shared.isNotLooping.value = true
- Observable.shared.loopStatusText.value = "⚠️ Not Looping!"
- Observable.shared.loopStatusColor.value = .yellow
- #if !targetEnvironment(macCatalyst)
- LiveActivityManager.shared.refreshFromCurrentState(reason: "notLooping")
- #endif
- } else {
- IsNotLooping = false
- Observable.shared.isNotLooping.value = false
- Observable.shared.loopStatusColor.value = .primary
- #if !targetEnvironment(macCatalyst)
- LiveActivityManager.shared.refreshFromCurrentState(reason: "loopingResumed")
- #endif
- }
- }
- // NS Device Status Response Processor
- func updateDeviceStatusDisplay(jsonDeviceStatus: [[String: AnyObject]]) {
- let previousIOBText = Observable.shared.iobText.value
- let previousDeviceWasLoop = Storage.shared.device.value == "Loop"
- infoManager.clearInfoData(types: [.iob, .cob, .battery, .pump, .pumpBattery, .target, .isf, .carbRatio, .updated, .recBolus, .tdd])
- // For Loop, clear the current override here - For Trio, it is handled using treatments
- if Storage.shared.device.value == "Loop" {
- infoManager.clearInfoData(types: [.override])
- }
- if jsonDeviceStatus.count == 0 {
- LogManager.shared.log(category: .deviceStatus, message: "Device status is empty")
- TaskScheduler.shared.rescheduleTask(id: .deviceStatus, to: Date().addingTimeInterval(5 * 60))
- return
- }
- // Process the current data first
- let lastDeviceStatus = jsonDeviceStatus[0] as [String: AnyObject]?
- // pump and uploader
- let formatter = ISO8601DateFormatter()
- formatter.formatOptions = [.withFullDate,
- .withTime,
- .withDashSeparatorInDate,
- .withColonSeparatorInTime]
- Observable.shared.previousAlertLastLoopTime.value = Observable.shared.alertLastLoopTime.value
- if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? {
- if let bolusIncrement = lastPumpRecord["bolusIncrement"] as? Double, bolusIncrement > 0 {
- Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: bolusIncrement)
- Storage.shared.bolusIncrementDetected.value = true
- } else if let model = lastPumpRecord["model"] as? String, model == "Dash" {
- Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: 0.05)
- Storage.shared.bolusIncrementDetected.value = true
- } else {
- Storage.shared.bolusIncrementDetected.value = false
- }
- if let clockString = lastPumpRecord["clock"] as? String,
- let lastPumpTime = formatter.date(from: clockString)?.timeIntervalSince1970
- {
- let storedTime = Observable.shared.alertLastLoopTime.value ?? 0
- if lastPumpTime > storedTime {
- Observable.shared.alertLastLoopTime.value = lastPumpTime
- Storage.shared.lastLoopTime.value = lastPumpTime
- }
- if let reservoirData = lastPumpRecord["reservoir"] as? Double {
- latestPumpVolume = reservoirData
- infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U", numericValue: reservoirData)
- Storage.shared.lastPumpReservoirU.value = reservoirData
- } else {
- // Pumps that only report "50+" get treated as exactly 50, both
- // for the volume alarm and for the info row's coloring.
- latestPumpVolume = 50.0
- infoManager.updateInfoData(type: .pump, value: "50+U", numericValue: 50.0)
- Storage.shared.lastPumpReservoirU.value = nil
- }
- }
- // Parse pump battery percentage
- if let pumpBatteryRecord = lastPumpRecord["battery"] as? [String: AnyObject],
- let pumpBatteryPercent = pumpBatteryRecord["percent"] as? Double
- {
- infoManager.updateInfoData(type: .pumpBattery, value: String(format: "%.0f", pumpBatteryPercent) + "%", numericValue: pumpBatteryPercent)
- Observable.shared.pumpBatteryLevel.value = pumpBatteryPercent
- }
- if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject],
- let upbat = uploader["battery"] as? Double
- {
- let isCharging = uploader["isCharging"] as? Bool
- let batteryText: String
- if isCharging == true {
- batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%"
- } else {
- batteryText = String(format: "%.0f", upbat) + "%"
- }
- infoManager.updateInfoData(type: .battery, value: batteryText, numericValue: upbat)
- Observable.shared.deviceBatteryLevel.value = upbat
- Observable.shared.deviceBatteryIsCharging.value = isCharging
- let timestamp = uploader["timestamp"] as? Date ?? Date()
- let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp)
- deviceBatteryData.append(currentBattery)
- // store only the last 30 battery readings
- if deviceBatteryData.count > 30 {
- deviceBatteryData.removeFirst()
- }
- }
- }
- // Loop - handle new data
- if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? {
- // Some pumps report no `pump.clock`; without it alertLastLoopTime stays 0
- // and the forecast anchors to epoch 0. Fall back to the loop cycle timestamp.
- if (lastDeviceStatus?["pump"] as? [String: AnyObject])?["clock"] == nil,
- let loopTimestampString = lastLoopRecord["timestamp"] as? String,
- let loopTimestamp = formatter.date(from: loopTimestampString)?.timeIntervalSince1970,
- loopTimestamp > (Observable.shared.alertLastLoopTime.value ?? 0)
- {
- Observable.shared.alertLastLoopTime.value = loopTimestamp
- Storage.shared.lastLoopTime.value = loopTimestamp
- }
- DeviceStatusLoop(formatter: formatter, lastLoopRecord: lastLoopRecord)
- var oText = ""
- currentOverride = 1.0
- if let lastOverride = lastDeviceStatus?["override"] as? [String: AnyObject],
- let isActive = lastOverride["active"] as? Bool, isActive
- {
- if let lastCorrection = lastOverride["currentCorrectionRange"] as? [String: AnyObject],
- let minValue = lastCorrection["minValue"] as? Double,
- let maxValue = lastCorrection["maxValue"] as? Double
- {
- if let multiplier = lastOverride["multiplier"] as? Double {
- currentOverride = multiplier
- oText += String(format: "%.0f%%", multiplier * 100)
- } else {
- oText += "100%"
- }
- oText += " ("
- oText += Localizer.toDisplayUnits(String(minValue)) + "-" + Localizer.toDisplayUnits(String(maxValue)) + ")"
- }
- infoManager.updateInfoData(type: .override, value: oText)
- } else {
- infoManager.clearInfoData(type: .override)
- }
- }
- // OpenAPS - handle new data
- if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String: AnyObject]? {
- DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord)
- }
- // If the active looping system flipped (Loop ⇄ Trio/OpenAPS), drop the previous
- // system's forecast so it doesn't linger next to the one just drawn above.
- let currentDeviceIsLoop = Storage.shared.device.value == "Loop"
- if currentDeviceIsLoop != previousDeviceWasLoop {
- if currentDeviceIsLoop {
- clearOpenAPSPredictionGraph()
- } else {
- clearLoopPredictionGraph()
- }
- }
- // Start the timer based on the timestamp
- let now = dateTimeUtils.getNowTimeIntervalUTC()
- let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0)
- DispatchQueue.main.async {
- var interval: Double
- if secondsAgo >= (20 * 60) {
- interval = 5 * 60
- } else if secondsAgo >= (10 * 60) {
- interval = 60
- } else if secondsAgo >= (7 * 60) {
- interval = 30
- } else if secondsAgo >= (5 * 60) {
- interval = 10
- } else {
- interval = 310 - secondsAgo
- TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(3))
- }
- if NightscoutSocketManager.shared.connectionState == .authenticated {
- interval = max(interval * 3, 60)
- }
- TaskScheduler.shared.rescheduleTask(
- id: .deviceStatus,
- to: Date().addingTimeInterval(interval)
- )
- }
- evaluateNotLooping()
- // Mark device status as loaded for initial loading state
- markDataLoaded("deviceStatus")
- if Storage.shared.contactEnabled.value, Storage.shared.contactIOB.value != .off,
- Observable.shared.iobText.value != previousIOBText
- {
- contactImageUpdater.updateContactImage(
- bgValue: Observable.shared.bgText.value,
- trend: Observable.shared.directionText.value,
- delta: Observable.shared.deltaText.value,
- iob: Observable.shared.iobText.value,
- stale: Observable.shared.bgStale.value
- )
- }
- LogManager.shared.log(category: .deviceStatus, message: "Update Device Status done", isDebug: true)
- }
- }
|