| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- // LoopFollow
- // AlarmTask.swift
- import Foundation
- extension MainViewController {
- func scheduleAlarmTask(initialDelay: TimeInterval = 30) {
- let firstRun = Date().addingTimeInterval(initialDelay)
- TaskScheduler.shared.scheduleTask(id: .alarmCheck, nextRun: firstRun) { [weak self] in
- guard let self = self else { return }
- self.alarmTaskAction()
- }
- }
- func alarmTaskAction() {
- DispatchQueue.main.async {
- let now = Date().timeIntervalSince1970
- let latestOverrideStart = self.overrideGraphData.last { $0.date <= now }?.date
- let latestOverrideEnd = self.overrideGraphData.last { $0.endDate <= now }?.endDate
- let latestTempTargetStart = self.tempTargetGraphData.last { $0.date <= now }?.date
- let latestTempTargetEnd = self.tempTargetGraphData.last { $0.endDate <= now }?.endDate
- let recBolus = Observable.shared.deviceRecBolus.value
- let COB = self.latestCOB?.value
- let sensorInsertedAt = Storage.shared.sageInsertTime.value
- let pumpInsertTime = Storage.shared.cageInsertTime.value
- let latestPumpVol = self.latestPumpVolume
- let bolusEntries = self.bolusData.map { BolusEntry(units: $0.value, date: Date(timeIntervalSince1970: $0.date)) }
- let latestBattery = Observable.shared.deviceBatteryLevel.value
- let latestPumpBattery = Observable.shared.pumpBatteryLevel.value
- let recentCarbs: [CarbSample] = self.carbData.map { CarbSample(grams: $0.value, date: Date(timeIntervalSince1970: $0.date)) }
- let alarmData = AlarmData(
- bgReadings: self.bgData
- .suffix(24)
- .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest
- predictionData: self.alarmPredictionData(), /// These are oldest .. newest
- expireDate: Storage.shared.expirationDate.value,
- lastLoopTime: Observable.shared.alertLastLoopTime.value,
- latestOverrideStart: latestOverrideStart,
- latestOverrideEnd: latestOverrideEnd,
- latestTempTargetStart: latestTempTargetStart,
- latestTempTargetEnd: latestTempTargetEnd,
- recBolus: recBolus,
- COB: COB,
- sageInsertTime: sensorInsertedAt,
- pumpInsertTime: pumpInsertTime,
- latestPumpVolume: latestPumpVol,
- IOB: self.latestIOB?.value,
- recentBoluses: bolusEntries,
- latestBattery: latestBattery,
- latestBatteryIsCharging: Observable.shared.deviceBatteryIsCharging.value,
- latestPumpBattery: latestPumpBattery,
- batteryHistory: self.deviceBatteryData,
- recentCarbs: recentCarbs,
- dbSizePercentage: Observable.shared.dbSizePercentage.value
- )
- let finalAlarmData: AlarmData
- if Observable.shared.debug.value {
- self.saveLatestAlarmDataToFile(alarmData)
- finalAlarmData = self.loadTestAlarmData() ?? alarmData
- } else {
- finalAlarmData = alarmData
- }
- LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(finalAlarmData)", isDebug: true)
- LogManager.shared.log(category: .alarm, message: "Alarms \(Storage.shared.alarms.value)", isDebug: true)
- AlarmManager.shared.checkAlarms(data: finalAlarmData)
- TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(60))
- }
- }
- /// Builds the forward glucose series the low alarm looks ahead in,
- /// oldest .. newest at 5-minute spacing.
- ///
- /// Loop reports a single forecast, already stored in `predictionData`. Trio
- /// reports four forecasts, collapsed into the lowest value per point in time
- /// by `lowestForecast(forecasts:start:cap:)`.
- func alarmPredictionData() -> [GlucoseValue] {
- if Storage.shared.device.value == "Loop" {
- return predictionData
- .prefix(MainViewController.alarmForecastPointCap)
- .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }
- }
- guard let predBGs = openAPSPredBGs else { return [] }
- let forecasts = ["ZT", "IOB", "COB", "UAM"].compactMap { predBGs[$0] }
- return MainViewController.lowestForecast(
- forecasts: forecasts,
- start: openAPSPredUpdatedTime ?? Date().timeIntervalSince1970
- )
- }
- /// Maximum number of forward points (5-minute spacing) the low alarm looks at:
- /// 12 points = 60 minutes, matching the predictive look-ahead's upper bound.
- static let alarmForecastPointCap = 12
- /// Collapses several forecasts into a single series by taking the **lowest**
- /// value at each point in time, oldest .. newest at 5-minute spacing.
- ///
- /// Trio/OpenAPS reports four forecasts (ZT, IOB, COB, UAM) rather than the
- /// single one Loop provides, so this lets the predictive-low alarm fire if
- /// *any* forecast dips to or below the threshold. Empty forecasts are ignored,
- /// and each point uses whichever forecasts still extend that far.
- static func lowestForecast(
- forecasts: [[Double]],
- start: TimeInterval,
- cap: Int = alarmForecastPointCap
- ) -> [GlucoseValue] {
- let nonEmpty = forecasts.filter { !$0.isEmpty }
- guard !nonEmpty.isEmpty else { return [] }
- let count = min(nonEmpty.map { $0.count }.max() ?? 0, cap)
- return (0 ..< count).compactMap { i in
- let valuesAtIndex = nonEmpty.compactMap { i < $0.count ? $0[i] : nil }
- guard let lowest = valuesAtIndex.min() else { return nil }
- return GlucoseValue(
- sgv: Int(lowest.rounded()),
- date: Date(timeIntervalSince1970: start + Double(i) * 300)
- )
- }
- }
- func saveLatestAlarmDataToFile(_ alarmData: AlarmData) {
- let encoder = JSONEncoder()
- encoder.dateEncodingStrategy = .iso8601
- do {
- let data = try encoder.encode(alarmData)
- let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("latestAlarmData.json")
- try data.write(to: url)
- } catch {
- LogManager.shared.log(category: .alarm, message: "Failed to save latest AlarmData: \(error)", isDebug: true)
- }
- }
- func loadTestAlarmData() -> AlarmData? {
- let fileManager = FileManager.default
- let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("testAlarmData.json")
- if fileManager.fileExists(atPath: url.path) {
- do {
- let data = try Data(contentsOf: url)
- let decoder = JSONDecoder()
- decoder.dateDecodingStrategy = .iso8601
- let alarmData = try decoder.decode(AlarmData.self, from: data)
- LogManager.shared.log(category: .alarm, message: "Loaded test AlarmData from \(url.path)", isDebug: true)
- return alarmData
- } catch {
- LogManager.shared.log(category: .alarm, message: "Failed to load test AlarmData: \(error)", isDebug: true)
- }
- }
- return nil
- }
- }
|