| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- import Combine
- import Foundation
- import LoopKit
- import LoopKitUI
- import SwiftDate
- import Swinject
- import UIKit
- protocol FetchGlucoseManager: SourceInfoProvider {
- func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
- func refreshCGM()
- func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
- func deleteGlucoseSource()
- func removeCalibrations()
- var glucoseSource: GlucoseSource! { get }
- var cgmManager: CGMManagerUI? { get }
- var cgmGlucoseSourceType: CGMType { get set }
- var cgmGlucosePluginId: String { get }
- var settingsManager: SettingsManager! { get }
- var shouldSyncToRemoteService: Bool { get }
- }
- extension FetchGlucoseManager {
- func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String) {
- updateGlucoseSource(cgmGlucoseSourceType: cgmGlucoseSourceType, cgmGlucosePluginId: cgmGlucosePluginId, newManager: nil)
- }
- }
- final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
- private let processQueue = DispatchQueue(label: "BaseGlucoseManager.processQueue")
- @Injected() var glucoseStorage: GlucoseStorage!
- @Injected() var nightscoutManager: NightscoutManager!
- @Injected() var tidepoolService: TidepoolManager!
- @Injected() var apsManager: APSManager!
- @Injected() var settingsManager: SettingsManager!
- @Injected() var healthKitManager: HealthKitManager!
- @Injected() var deviceDataManager: DeviceDataManager!
- @Injected() var pluginCGMManager: PluginManager!
- @Injected() var calibrationService: CalibrationService!
- private var lifetime = Lifetime()
- private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
- var cgmGlucoseSourceType: CGMType = .none
- var cgmGlucosePluginId: String = ""
- var cgmManager: CGMManagerUI? {
- didSet {
- rawCGMManager = cgmManager?.rawValue
- UserDefaults.standard.clearLegacyCGMManagerRawValue()
- }
- }
- @PersistedProperty(key: "CGMManagerState") var rawCGMManager: CGMManager.RawValue?
- private lazy var simulatorSource = GlucoseSimulatorSource()
- var shouldSyncToRemoteService: Bool {
- guard let cgmManager = cgmManager else {
- return true
- }
- return cgmManager.shouldSyncToRemoteService
- }
- init(resolver: Resolver) {
- injectServices(resolver)
- // init at the start of the app
- cgmGlucoseSourceType = settingsManager.settings.cgm
- cgmGlucosePluginId = settingsManager.settings.cgmPluginIdentifier
- // load cgmManager
- updateGlucoseSource(
- cgmGlucoseSourceType: settingsManager.settings.cgm,
- cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier
- )
- subscribe()
- }
- var glucoseSource: GlucoseSource!
- func removeCalibrations() {
- calibrationService.removeAllCalibrations()
- }
- func deleteGlucoseSource() {
- cgmManager = nil
- updateGlucoseSource(
- cgmGlucoseSourceType: CGMType.none,
- cgmGlucosePluginId: ""
- )
- }
- func saveConfigManager() {
- guard let cgmM = cgmManager else {
- return
- }
- // save the config in rawCGMManager
- rawCGMManager = cgmM.rawValue
- // sync with upload glucose
- settingsManager.settings.uploadGlucose = cgmM.shouldSyncToRemoteService
- }
- func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
- // if changed, remove all calibrations
- if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
- removeCalibrations()
- cgmManager = nil
- }
- self.cgmGlucoseSourceType = cgmGlucoseSourceType
- self.cgmGlucosePluginId = cgmGlucosePluginId
- // if not plugin, manager is not changed and stay with the "old" value if the user come back to previous cgmtype
- // if plugin, if the same pluginID, no change required because the manager is available
- // if plugin, if not the same pluginID, need to reset the cgmManager
- // if plugin and newManager provides, update cgmManager
- debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
- if let manager = newManager
- {
- cgmManager = manager
- removeCalibrations()
- } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
- cgmManager = cgmManagerFromRawValue(rawCGMManager)
- } else {
- saveConfigManager()
- }
- switch self.cgmGlucoseSourceType {
- case .none:
- glucoseSource = nil
- case .xdrip:
- glucoseSource = AppGroupSource(from: "xDrip", cgmType: .xdrip)
- case .nightscout:
- glucoseSource = nightscoutManager
- case .simulator:
- glucoseSource = simulatorSource
- case .glucoseDirect:
- glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
- case .enlite:
- glucoseSource = deviceDataManager
- case .plugin:
- glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
- }
- // update the config
- }
- /// Upload cgmManager from raw value
- func cgmManagerFromRawValue(_ rawValue: [String: Any]) -> CGMManagerUI? {
- guard let rawState = rawValue["state"] as? CGMManager.RawStateValue,
- let Manager = pluginCGMManager.getCGMManagerTypeByIdentifier(cgmGlucosePluginId)
- else {
- return nil
- }
- return Manager.init(rawState: rawState)
- }
- /// function called when a callback is fired by CGM BLE - no more used
- public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
- let syncDate = glucoseStorage.syncDate()
- debug(.deviceManager, "CGM BLE FETCHGLUCOSE : SyncDate is \(syncDate)")
- glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
- }
- /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
- public func refreshCGM() {
- debug(.deviceManager, "refreshCGM by pump")
- // updateGlucoseSource(cgmGlucoseSourceType: settingsManager.settings.cgm, cgmGlucosePluginId: settingsManager.settings.cgmPluginIdentifier)
- Publishers.CombineLatest3(
- Just(glucoseStorage.syncDate()),
- healthKitManager.fetch(nil),
- glucoseSource.fetchIfNeeded()
- )
- .eraseToAnyPublisher()
- .receive(on: processQueue)
- .sink { syncDate, glucoseFromHealth, glucose in
- debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
- self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose, glucoseFromHealth: glucoseFromHealth)
- }
- .store(in: &lifetime)
- }
- private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose], glucoseFromHealth: [BloodGlucose] = []) {
- // calibration add if required only for sensor
- let newGlucose = overcalibrate(entries: glucose)
- let allGlucose = newGlucose + glucoseFromHealth
- var filteredByDate: [BloodGlucose] = []
- var filtered: [BloodGlucose] = []
- // start background time extension
- var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
- backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
- guard let bg = backGroundFetchBGTaskID else { return }
- UIApplication.shared.endBackgroundTask(bg)
- backGroundFetchBGTaskID = .invalid
- }
- guard allGlucose.isNotEmpty else {
- if let backgroundTask = backGroundFetchBGTaskID {
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backGroundFetchBGTaskID = .invalid
- }
- return
- }
- filteredByDate = allGlucose.filter { $0.dateString > syncDate }
- filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
- guard filtered.isNotEmpty else {
- // end of the BG tasks
- if let backgroundTask = backGroundFetchBGTaskID {
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backGroundFetchBGTaskID = .invalid
- }
- return
- }
- debug(.deviceManager, "New glucose found")
- // filter the data if it is the case
- if settingsManager.settings.smoothGlucose {
- // limit to 30 minutes of previous BG Data
- let oldGlucoses = glucoseStorage.recent().filter {
- $0.dateString.addingTimeInterval(31 * 60) > Date()
- }
- var smoothedValues = oldGlucoses + filtered
- // smooth with 3 repeats
- for _ in 1 ... 3 {
- smoothedValues.smoothSavitzkyGolayQuaDratic(withFilterWidth: 3)
- }
- // find the new values only
- filtered = smoothedValues.filter { $0.dateString > syncDate }
- }
- glucoseStorage.storeGlucose(filtered)
- deviceDataManager.heartbeat(date: Date())
- nightscoutManager.uploadGlucose()
- tidepoolService.uploadGlucose(device: cgmManager?.cgmManagerStatus.device)
- let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
- guard glucoseForHealth.isNotEmpty else {
- // end of the BG tasks
- if let backgroundTask = backGroundFetchBGTaskID {
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backGroundFetchBGTaskID = .invalid
- }
- return
- }
- healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
- // end of the BG tasks
- if let backgroundTask = backGroundFetchBGTaskID {
- UIApplication.shared.endBackgroundTask(backgroundTask)
- backGroundFetchBGTaskID = .invalid
- }
- }
- /// The function used to start the timer sync - Function of the variable defined in config
- private func subscribe() {
- timer.publisher
- .receive(on: processQueue)
- .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
- debug(.nightscout, "FetchGlucoseManager timer heartbeat")
- if let glucoseSource = self.glucoseSource {
- return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
- } else {
- return Empty(completeImmediately: false).eraseToAnyPublisher()
- }
- }
- .sink { glucose in
- debug(.nightscout, "FetchGlucoseManager callback sensor")
- Publishers.CombineLatest3(
- Just(glucose),
- Just(self.glucoseStorage.syncDate()),
- self.healthKitManager.fetch(nil)
- )
- .eraseToAnyPublisher()
- .sink { newGlucose, syncDate, glucoseFromHealth in
- self.glucoseStoreAndHeartDecision(
- syncDate: syncDate,
- glucose: newGlucose,
- glucoseFromHealth: glucoseFromHealth
- )
- }
- .store(in: &self.lifetime)
- }
- .store(in: &lifetime)
- timer.fire()
- timer.resume()
- }
- func sourceInfo() -> [String: Any]? {
- glucoseSource.sourceInfo()
- }
- private func overcalibrate(entries: [BloodGlucose]) -> [BloodGlucose] {
- // overcalibrate
- var overcalibration: ((Int) -> (Double))?
- if let cal = calibrationService {
- overcalibration = cal.calibrate
- }
- if let overcalibration = overcalibration {
- return entries.map { entry in
- var entry = entry
- entry.glucose = Int(overcalibration(entry.glucose!))
- entry.sgv = Int(overcalibration(entry.sgv!))
- return entry
- }
- } else {
- return entries
- }
- }
- }
- extension CGMManager {
- typealias RawValue = [String: Any]
- var rawValue: [String: Any] {
- [
- "managerIdentifier": pluginIdentifier,
- "state": rawState
- ]
- }
- }
|