import Combine import Foundation import LoopKit import LoopKitUI import MinimedKit import MockKit import OmniKit import SwiftDate import Swinject import UserNotifications protocol DeviceDataManager { var pumpManager: PumpManagerUI? { 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, force: Bool) 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(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() 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) } pumpManager.setMustProvideBLEHeartbeat(true) } else { pumpDisplayState.value = 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() { if let pumpManagerRawValue = UserDefaults.standard.pumpManagerRawValue { pumpManager = pumpManagerFromRawValue(pumpManagerRawValue) } } @SyncAccess(lock: accessLock) private var pumpUpdateInProgress = false func createBolusProgressReporter() -> DoseProgressReporter? { pumpManager?.createBolusProgressReporter(reportingOn: processQueue) } func heartbeat(date: Date, force: Bool) { if force { updatePumpData() return } var updateInterval: TimeInterval = 5.minutes.timeInterval switch lastHeartBeatTime.timeIntervalSince(date) { case let interval where interval < -10.minutes.timeInterval: break case let interval where interval < -5.minutes.timeInterval: updateInterval = 1.minutes.timeInterval default: break } let interval = date.timeIntervalSince(lastHeartBeatTime) guard interval >= updateInterval else { debug(.deviceManager, "Last hearbeat \(interval / 60) min ago, skip updating the pump data") return } lastHeartBeatTime = date updatePumpData() } private func updatePumpData() { guard let pumpManager = pumpManager else { debug(.deviceManager, "Pump is not set, skip updating") return } guard !pumpUpdateInProgress else { debug(.deviceManager, "Pump update in progress, skip updating") return } debug(.deviceManager, "Start updating the pump data") pumpUpdateInProgress = true pumpManager.ensureCurrentPumpData { debug(.deviceManager, "Pump Data updated") self.pumpUpdateInProgress = false } } 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] } } 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") pumpUpdateInProgress = false heartbeat(date: Date(), force: false) } func pumpManagerMustProvideBLEHeartbeat(_: PumpManager) -> Bool { true } func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) { 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() } 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) { pumpManager = nil } func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {} func pumpManager(_: PumpManager, didError error: PumpManagerError) { debug(.deviceManager, "error: \(error.localizedDescription), reason: \(String(describing: error.failureReason))") errorSubject.send(error) pumpUpdateInProgress = false } 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)) pumpUpdateInProgress = false debug(.deviceManager, "Recomends loop") recommendsLoop.send() } 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, message) } } // 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) }