import Algorithms import Combine import Foundation import LoopKit import LoopKitUI import MinimedKit import MockKit import OmniBLE import OmniKit import ShareClient import SwiftDate import Swinject import UserNotifications protocol DeviceDataManager: GlucoseSource { var pumpManager: PumpManagerUI? { get set } var bluetoothManager: BluetoothStateManager { get } var loopInProgress: Bool { get set } var pumpDisplayState: CurrentValueSubject { get } var recommendsLoop: PassthroughSubject { get } var bolusTrigger: PassthroughSubject { get } var manualTempBasal: PassthroughSubject { get } var errorSubject: PassthroughSubject { get } var pumpName: CurrentValueSubject { get } var pumpExpiresAtDate: CurrentValueSubject { get } func heartbeat(date: Date) func createBolusProgressReporter() -> DoseProgressReporter? var alertHistoryStorage: AlertHistoryStorage! { get } } private let staticPumpManagers: [PumpManagerUI.Type] = [ MinimedPumpManager.self, OmnipodPumpManager.self, OmniBLEPumpManager.self, MockPumpManager.self ] private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [ MinimedPumpManager.pluginIdentifier: MinimedPumpManager.self, OmnipodPumpManager.pluginIdentifier: OmnipodPumpManager.self, OmniBLEPumpManager.pluginIdentifier: OmniBLEPumpManager.self, MockPumpManager.pluginIdentifier: MockPumpManager.self ] 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() var alertHistoryStorage: AlertHistoryStorage! @Injected() private var storage: FileStorage! @Injected() private var broadcaster: Broadcaster! @Injected() private var glucoseStorage: GlucoseStorage! @Injected() private var settingsManager: SettingsManager! @Injected() private var bluetoothProvider: BluetoothStateManager! @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() let manualTempBasal = PassthroughSubject() private let router = FreeAPSApp.resolver.resolve(Router.self)! @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 rawPumpManager = pumpManager?.rawValue UserDefaults.standard.clearLegacyPumpManagerRawValue() 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) } if let omnipodBLE = pumpManager as? OmniBLEPumpManager { guard let endTime = omnipodBLE.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return } pumpExpiresAtDate.send(endTime) } if let simulatorPump = pumpManager as? MockPumpManager { pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage) pumpName.send(simulatorPump.localizedTitle) storage.save(Decimal(simulatorPump.pumpReservoirCapacity), as: OpenAPS.Monitor.reservoir) DispatchQueue.main.async { self.broadcaster.notify(PumpReservoirObserver.self, on: .main) { $0.pumpReservoirDidChange(Decimal(simulatorPump.state.reservoirUnitsRemaining)) } } let batteryPercent = Int((simulatorPump.state.pumpBatteryChargeRemaining ?? 1) * 100) let battery = Battery( percent: batteryPercent, voltage: nil, string: batteryPercent >= 10 ? .normal : .low, display: simulatorPump.state.pumpBatteryChargeRemaining != nil ) storage.save(battery, as: OpenAPS.Monitor.battery) DispatchQueue.main.async { self.broadcaster.notify(PumpBatteryObserver.self, on: .main) { $0.pumpBatteryDidChange(battery) } } } } else { pumpDisplayState.value = nil pumpExpiresAtDate.send(nil) pumpName.send("") } } } @PersistedProperty(key: "PumpManagerState") var rawPumpManager: PumpManager.RawValue? var bluetoothManager: BluetoothStateManager { bluetoothProvider } 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 broadcaster.register(AlertObserver.self, observer: self) } func setupPumpManager() { if let pumpManagerRawValue = rawPumpManager ?? UserDefaults.standard.legacyPumpManagerRawValue { pumpManager = pumpManagerFromRawValue(pumpManagerRawValue) } else { pumpManager = nil } } 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") processQueue.safeSync { pumpManager.ensureCurrentPumpData { _ in debug(.deviceManager, "Pump data updated.") self.updateUpdateFinished(true) } } } 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.") } 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 var glucoseManager: FetchGlucoseManager? var cgmManager: CGMManagerUI? var cgmType: CGMType = .enlite func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> { fetch(nil) } func fetch(_: DispatchTimer?) -> 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 .unreliableData: debug(.deviceManager, "Unreliable data received") 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: Decimal(value), 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() } } // MARK: - PumpManagerDelegate extension BaseDeviceDataManager: PumpManagerDelegate { var automaticDosingEnabled: Bool { settingsManager.settings.closedLoop // Take if close or open loop } func pumpManager( _: LoopKit.PumpManager, didRequestBasalRateScheduleChange _: LoopKit.BasalRateSchedule, completion _: @escaping (Error?) -> Void ) { debug(.deviceManager, "pumpManagerBasalRateChange") } func pumpManagerPumpWasReplaced(_: PumpManager) { debug(.deviceManager, "pumpManagerPumpWasReplaced") } var detectedSystemTimeOffset: TimeInterval { // trustedTimeChecker.detectedSystemTimeOffset 0 } func pumpManager(_: PumpManager, didAdjustPumpClockBy adjustment: TimeInterval) { debug(.deviceManager, "didAdjustPumpClockBy \(adjustment)") } func pumpManagerDidUpdateState(_ pumpManager: PumpManager) { rawPumpManager = pumpManager.rawValue if self.pumpManager == nil, let newPumpManager = pumpManager as? PumpManagerUI { self.pumpManager = newPumpManager } pumpName.send(pumpManager.localizedTitle) } /// heartbeat with pump occurs some issues in the backgroundtask - so never used 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 if status.bolusState != .canceling { bolusTrigger.send(false) } if status.insulinType != oldStatus.insulinType { settingsManager.updateInsulinCurve(status.insulinType) } 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 reservoirVal = omnipod.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF // TODO: find the value Pod.maximumReservoirReading let reservoir = Decimal(reservoirVal) > 50.0 ? 0xDEAD_BEEF : reservoirVal storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir) broadcaster.notify(PumpReservoirObserver.self, on: processQueue) { $0.pumpReservoirDidChange(Decimal(reservoir)) } if let tempBasal = omnipod.state.podState?.unfinalizedTempBasal, !tempBasal.isFinished(), !tempBasal.automatic { // the manual basal temp is launch - block every thing debug(.deviceManager, "manual temp basal") manualTempBasal.send(true) } else { // no more manual Temp Basal ! manualTempBasal.send(false) } guard let endTime = omnipod.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return } pumpExpiresAtDate.send(endTime) if let startTime = omnipod.state.podState?.activatedAt { storage.save(startTime, as: OpenAPS.Monitor.podAge) } } if let omnipodBLE = pumpManager as? OmniBLEPumpManager { let reservoirVal = omnipodBLE.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF // TODO: find the value Pod.maximumReservoirReading let reservoir = Decimal(reservoirVal) > 50.0 ? 0xDEAD_BEEF : reservoirVal storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir) broadcaster.notify(PumpReservoirObserver.self, on: processQueue) { $0.pumpReservoirDidChange(Decimal(reservoir)) } // manual temp basal on if let tempBasal = omnipodBLE.state.podState?.unfinalizedTempBasal, !tempBasal.isFinished(), !tempBasal.automatic { // the manual basal temp is launch - block every thing debug(.deviceManager, "manual temp basal") manualTempBasal.send(true) } else { // no more manual Temp Basal ! manualTempBasal.send(false) } guard let endTime = omnipodBLE.state.podState?.expiresAt else { pumpExpiresAtDate.send(nil) return } pumpExpiresAtDate.send(endTime) if let startTime = omnipodBLE.state.podState?.activatedAt { storage.save(startTime, as: OpenAPS.Monitor.podAge) } } } func pumpManagerWillDeactivate(_: PumpManager) { dispatchPrecondition(condition: .onQueue(processQueue)) pumpManager = nil broadcaster.notify(PumpDeactivatedObserver.self, on: processQueue) { $0.pumpDeactivatedDidChange() } } 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?, replacePendingEvents _: Bool, completion: @escaping (_ error: Error?) -> Void ) { dispatchPrecondition(condition: .onQueue(processQueue)) debug(.deviceManager, "New pump events:\n\(events.map(\.title).joined(separator: "\n"))") // filter buggy TBRs > maxBasal from MDT let events = events.filter { // type is optional... guard let type = $0.type, type == .tempBasal else { return true } return $0.dose?.unitsPerHour ?? 0 <= Double(settingsManager.pumpSettings.maxBasal) } 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 issueAlert(_ alert: Alert) { alertHistoryStorage.storeAlert( AlertEntry( alertIdentifier: alert.identifier.alertIdentifier, primitiveInterruptionLevel: alert.interruptionLevel.storedValue as? Decimal, issuedDate: Date(), managerIdentifier: alert.identifier.managerIdentifier, triggerType: alert.trigger.storedType, triggerInterval: alert.trigger.storedInterval as? Decimal, contentTitle: alert.foregroundContent?.title, contentBody: alert.foregroundContent?.body ) ) } func retractAlert(identifier: Alert.Identifier) { alertHistoryStorage.deleteAlert(identifier: identifier.alertIdentifier) } func doesIssuedAlertExist(identifier _: Alert.Identifier, completion _: @escaping (Result) -> Void) { debug(.deviceManager, "doesIssueAlertExist") } func lookupAllUnretracted(managerIdentifier _: String, completion _: @escaping (Result<[PersistedAlert], Error>) -> Void) { debug(.deviceManager, "lookupAllUnretracted") } func lookupAllUnacknowledgedUnretracted( managerIdentifier _: String, completion _: @escaping (Result<[PersistedAlert], Error>) -> Void ) {} func recordRetractedAlert(_: Alert, at _: Date) {} 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)") } } // MARK: - CGMManagerDelegate 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 cgmManager(_: LoopKit.CGMManager, hasNew _: [LoopKit.PersistedCgmEvent]) {} func cgmManagerWantsDeletion(_: CGMManager) {} func cgmManagerDidUpdateState(_: CGMManager) {} func credentialStoragePrefix(for _: CGMManager) -> String { "BaseDeviceDataManager" } func cgmManager(_: CGMManager, didUpdate _: CGMManagerStatus) {} } // MARK: - AlertPresenter extension BaseDeviceDataManager: AlertObserver { func AlertDidUpdate(_ alerts: [AlertEntry]) { alerts.forEach { alert in if alert.acknowledgedDate == nil { ackAlert(alert: alert) } } } private func ackAlert(alert: AlertEntry) { let typeMessage: MessageType let alertUp = alert.alertIdentifier.uppercased() if alertUp.contains("FAULT") || alertUp.contains("ERROR") { typeMessage = .errorPump } else { typeMessage = .warning } let messageCont = MessageContent(content: alert.contentBody ?? "Unknown", type: typeMessage) let alertIssueDate = alert.issuedDate processQueue.async { // if not alert in OmniPod/BLE, the acknowledgeAlert didn't do callbacks- Hack to manage this case if let omnipodBLE = self.pumpManager as? OmniBLEPumpManager { if omnipodBLE.state.activeAlerts.isEmpty { // force to ack alert in the alertStorage self.alertHistoryStorage.ackAlert(alertIssueDate, nil) } } if let omniPod = self.pumpManager as? OmnipodPumpManager { if omniPod.state.activeAlerts.isEmpty { // force to ack alert in the alertStorage self.alertHistoryStorage.ackAlert(alertIssueDate, nil) } } self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.alertIdentifier) { error in self.router.alertMessage.send(messageCont) if let error = error { self.alertHistoryStorage.ackAlert(alertIssueDate, error.localizedDescription) debug(.deviceManager, "acknowledge not succeeded with error \(error.localizedDescription)") } else { self.alertHistoryStorage.ackAlert(alertIssueDate, nil) } } self.broadcaster.notify(pumpNotificationObserver.self, on: self.processQueue) { $0.pumpNotification(alert: alert) } } } } // 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) } protocol PumpDeactivatedObserver { func pumpDeactivatedDidChange() }