import Algorithms import Combine import Foundation import LoopKit import LoopKitUI import MinimedKit import MockKit import OmniKit import SwiftDate import Swinject import UserNotifications protocol DeviceDataManager: GlucoseSource { var pumpManager: PumpManagerUI? { get set } var loopInProgress: Bool { get set } var pumpDisplayState: CurrentValueSubject { get } var recommendsLoop: PassthroughSubject { get } var bolusTrigger: PassthroughSubject { get } var errorSubject: PassthroughSubject { get } var pumpName: CurrentValueSubject { get } var pumpExpiresAtDate: CurrentValueSubject { get } func heartbeat(date: Date) func createBolusProgressReporter() -> DoseProgressReporter? } private let staticPumpManagers: [PumpManagerUI.Type] = [ MinimedPumpManager.self, OmnipodPumpManager.self, MockPumpManager.self ] private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = staticPumpManagers.reduce(into: [:]) { map, Type in map[Type.managerIdentifier] = Type } private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock") final class BaseDeviceDataManager: DeviceDataManager, Injectable { private let processQueue = DispatchQueue.markedQueue(label: "BaseDeviceDataManager.processQueue") @Injected() private var pumpHistoryStorage: PumpHistoryStorage! @Injected() private var storage: FileStorage! @Injected() private var broadcaster: Broadcaster! @Injected() private var glucoseStorage: GlucoseStorage! @Persisted(key: "BaseDeviceDataManager.lastEventDate") var lastEventDate: Date? = nil @SyncAccess(lock: accessLock) @Persisted(key: "BaseDeviceDataManager.lastHeartBeatTime") var lastHeartBeatTime: Date = .distantPast let recommendsLoop = PassthroughSubject() let bolusTrigger = PassthroughSubject() let errorSubject = PassthroughSubject() let pumpNewStatus = PassthroughSubject() @SyncAccess private var pumpUpdateCancellable: AnyCancellable? private var pumpUpdatePromise: Future.Promise? @SyncAccess var loopInProgress: Bool = false var pumpManager: PumpManagerUI? { didSet { pumpManager?.pumpManagerDelegate = self pumpManager?.delegateQueue = processQueue UserDefaults.standard.pumpManagerRawValue = pumpManager?.rawValue if let pumpManager = pumpManager { pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage) pumpName.send(pumpManager.localizedTitle) if let omnipod = pumpManager as? OmnipodPumpManager { guard let endTime = omnipod.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return } pumpExpiresAtDate.send(endTime) } } else { pumpDisplayState.value = nil pumpExpiresAtDate.send(nil) pumpName.send("") } } } var hasBLEHeartbeat: Bool { (pumpManager as? MockPumpManager) == nil } let pumpDisplayState = CurrentValueSubject(nil) let pumpExpiresAtDate = CurrentValueSubject(nil) let pumpName = CurrentValueSubject("Pump") init(resolver: Resolver) { injectServices(resolver) setupPumpManager() UIDevice.current.isBatteryMonitoringEnabled = true } func setupPumpManager() { pumpManager = UserDefaults.standard.pumpManagerRawValue.flatMap { pumpManagerFromRawValue($0) } } func createBolusProgressReporter() -> DoseProgressReporter? { pumpManager?.createBolusProgressReporter(reportingOn: processQueue) } func heartbeat(date: Date) { guard pumpUpdateCancellable == nil else { warning(.deviceManager, "Pump updating already in progress. Skip updating.") return } guard !loopInProgress else { warning(.deviceManager, "Loop in progress. Skip updating.") return } func update(_: Future.Promise?) {} processQueue.safeSync { lastHeartBeatTime = date updatePumpData() } } private func updatePumpData() { guard let pumpManager = pumpManager else { debug(.deviceManager, "Pump is not set, skip updating") updateUpdateFinished(false) return } debug(.deviceManager, "Start updating the pump data") pumpUpdateCancellable = Future { [unowned self] promise in pumpUpdatePromise = promise debug(.deviceManager, "Waiting for pump update and loop recommendation") processQueue.safeSync { pumpManager.ensureCurrentPumpData { debug(.deviceManager, "Pump data updated.") } } } .timeout(60, scheduler: processQueue) .replaceError(with: false) .replaceEmpty(with: false) .sink(receiveValue: updateUpdateFinished) } private func updateUpdateFinished(_ recommendsLoop: Bool) { pumpUpdateCancellable = nil pumpUpdatePromise = nil if !recommendsLoop { warning(.deviceManager, "Loop recommendation time out or got error. Trying to loop right now.") } guard !loopInProgress else { warning(.deviceManager, "Loop already in progress. Skip recommendation.") return } self.recommendsLoop.send() } private func pumpManagerFromRawValue(_ rawValue: [String: Any]) -> PumpManagerUI? { guard let rawState = rawValue["state"] as? PumpManager.RawStateValue, let Manager = pumpManagerTypeFromRawValue(rawValue) else { return nil } return Manager.init(rawState: rawState) as? PumpManagerUI } private func pumpManagerTypeFromRawValue(_ rawValue: [String: Any]) -> PumpManager.Type? { guard let managerIdentifier = rawValue["managerIdentifier"] as? String else { return nil } return staticPumpManagersByIdentifier[managerIdentifier] } // MARK: - GlucoseSource @Persisted(key: "BaseDeviceDataManager.lastFetchGlucoseDate") private var lastFetchGlucoseDate: Date = .distantPast func fetch() -> AnyPublisher<[BloodGlucose], Never> { guard let medtronic = pumpManager as? MinimedPumpManager else { warning(.deviceManager, "Fetch minilink glucose failed: Pump is not Medtronic") return Just([]).eraseToAnyPublisher() } guard lastFetchGlucoseDate.addingTimeInterval(5.minutes.timeInterval) < Date() else { return Just([]).eraseToAnyPublisher() } medtronic.cgmManagerDelegate = self return Future<[BloodGlucose], Error> { promise in self.processQueue.async { medtronic.fetchNewDataIfNeeded { result in switch result { case .noData: debug(.deviceManager, "Minilink glucose is empty") promise(.success([])) case let .newData(glucose): let directions: [BloodGlucose.Direction?] = [nil] + glucose.windows(ofCount: 2).map { window -> BloodGlucose.Direction? in let pair = Array(window) guard pair.count == 2 else { return nil } let firstValue = Int(pair[0].quantity.doubleValue(for: .milligramsPerDeciliter)) let secondValue = Int(pair[1].quantity.doubleValue(for: .milligramsPerDeciliter)) return .init(trend: secondValue - firstValue) } let results = glucose.enumerated().map { index, sample -> BloodGlucose in let value = Int(sample.quantity.doubleValue(for: .milligramsPerDeciliter)) return BloodGlucose( _id: sample.syncIdentifier, sgv: value, direction: directions[index], date: Decimal(Int(sample.date.timeIntervalSince1970 * 1000)), dateString: sample.date, unfiltered: nil, filtered: nil, noise: nil, glucose: value, type: "sgv" ) } if let lastDate = results.last?.dateString { self.lastFetchGlucoseDate = lastDate } promise(.success(results)) case let .error(error): warning(.deviceManager, "Fetch minilink glucose failed", error: error) promise(.failure(error)) } } } } .timeout(60 * 3, scheduler: processQueue, options: nil, customError: nil) .replaceError(with: []) .replaceEmpty(with: []) .eraseToAnyPublisher() } } extension BaseDeviceDataManager: PumpManagerDelegate { func pumpManager(_: PumpManager, didAdjustPumpClockBy adjustment: TimeInterval) { debug(.deviceManager, "didAdjustPumpClockBy \(adjustment)") } func pumpManagerDidUpdateState(_ pumpManager: PumpManager) { UserDefaults.standard.pumpManagerRawValue = pumpManager.rawValue if self.pumpManager == nil, let newPumpManager = pumpManager as? PumpManagerUI { self.pumpManager = newPumpManager } pumpName.send(pumpManager.localizedTitle) } func pumpManagerBLEHeartbeatDidFire(_: PumpManager) { debug(.deviceManager, "Pump Heartbeat: do nothing. Pump connection is OK") } func pumpManagerMustProvideBLEHeartbeat(_: PumpManager) -> Bool { true } func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) { dispatchPrecondition(condition: .onQueue(processQueue)) debug(.deviceManager, "New pump status Bolus: \(status.bolusState)") debug(.deviceManager, "New pump status Basal: \(String(describing: status.basalDeliveryState))") if case .inProgress = status.bolusState { bolusTrigger.send(true) } else { bolusTrigger.send(false) } let batteryPercent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100) let battery = Battery( percent: batteryPercent, voltage: nil, string: batteryPercent >= 10 ? .normal : .low, display: pumpManager.status.pumpBatteryChargeRemaining != nil ) storage.save(battery, as: OpenAPS.Monitor.battery) broadcaster.notify(PumpBatteryObserver.self, on: processQueue) { $0.pumpBatteryDidChange(battery) } if let omnipod = pumpManager as? OmnipodPumpManager { let reservoir = omnipod.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir) broadcaster.notify(PumpReservoirObserver.self, on: processQueue) { $0.pumpReservoirDidChange(Decimal(reservoir)) } guard let endTime = omnipod.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return } pumpExpiresAtDate.send(endTime) } } func pumpManagerWillDeactivate(_: PumpManager) { dispatchPrecondition(condition: .onQueue(processQueue)) pumpManager = nil } func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {} func pumpManager(_: PumpManager, didError error: PumpManagerError) { dispatchPrecondition(condition: .onQueue(processQueue)) debug(.deviceManager, "error: \(error.localizedDescription), reason: \(String(describing: error.failureReason))") errorSubject.send(error) } func pumpManager( _: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation _: Date?, completion: @escaping (_ error: Error?) -> Void ) { dispatchPrecondition(condition: .onQueue(processQueue)) debug(.deviceManager, "New pump events:\n\(events.map(\.title).joined(separator: "\n"))") pumpHistoryStorage.storePumpEvents(events) lastEventDate = events.last?.date completion(nil) } func pumpManager( _: PumpManager, didReadReservoirValue units: Double, at date: Date, completion: @escaping (Result< (newValue: ReservoirValue, lastValue: ReservoirValue?, areStoredValuesContinuous: Bool), Error >) -> Void ) { dispatchPrecondition(condition: .onQueue(processQueue)) debug(.deviceManager, "Reservoir Value \(units), at: \(date)") storage.save(Decimal(units), as: OpenAPS.Monitor.reservoir) broadcaster.notify(PumpReservoirObserver.self, on: processQueue) { $0.pumpReservoirDidChange(Decimal(units)) } completion(.success(( newValue: Reservoir(startDate: Date(), unitVolume: units), lastValue: nil, areStoredValuesContinuous: true ))) } func pumpManagerRecommendsLoop(_: PumpManager) { dispatchPrecondition(condition: .onQueue(processQueue)) debug(.deviceManager, "Pump recommends loop") guard let promise = pumpUpdatePromise else { warning(.deviceManager, "We do not waiting for loop recommendation at this time.") return } promise(.success(true)) } func startDateToFilterNewPumpEvents(for _: PumpManager) -> Date { lastEventDate?.addingTimeInterval(-15.minutes.timeInterval) ?? Date().addingTimeInterval(-2.hours.timeInterval) } } // MARK: - DeviceManagerDelegate extension BaseDeviceDataManager: DeviceManagerDelegate { func scheduleNotification( for _: DeviceManager, identifier: String, content: UNNotificationContent, trigger: UNNotificationTrigger? ) { let request = UNNotificationRequest( identifier: identifier, content: content, trigger: trigger ) DispatchQueue.main.async { UNUserNotificationCenter.current().add(request) } } func clearNotification(for _: DeviceManager, identifier: String) { DispatchQueue.main.async { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier]) } } func removeNotificationRequests(for _: DeviceManager, identifiers: [String]) { DispatchQueue.main.async { UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers) } } func deviceManager( _: DeviceManager, logEventForDeviceIdentifier _: String?, type _: DeviceLogEntryType, message: String, completion _: ((Error?) -> Void)? ) { debug(.deviceManager, "Device message: \(message)") } } extension BaseDeviceDataManager: CGMManagerDelegate { func startDateToFilterNewData(for _: CGMManager) -> Date? { glucoseStorage.syncDate().addingTimeInterval(-10.minutes.timeInterval) // additional time to calculate directions } func cgmManager(_: CGMManager, hasNew _: CGMReadingResult) {} func cgmManagerWantsDeletion(_: CGMManager) {} func cgmManagerDidUpdateState(_: CGMManager) {} func credentialStoragePrefix(for _: CGMManager) -> String { "BaseDeviceDataManager" } func cgmManager(_: CGMManager, didUpdate _: CGMManagerStatus) {} } // MARK: - AlertPresenter extension BaseDeviceDataManager: AlertPresenter { func issueAlert(_: Alert) {} func retractAlert(identifier _: Alert.Identifier) {} } // MARK: Others protocol PumpReservoirObserver { func pumpReservoirDidChange(_ reservoir: Decimal) } protocol PumpBatteryObserver { func pumpBatteryDidChange(_ battery: Battery) }