Explorar el Código

Merge branch 'avouspierre/dash_dev' into dontTest

Jon B.M hace 3 años
padre
commit
893e0dd716
Se han modificado 24 ficheros con 487 adiciones y 96 borrados
  1. 8 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 1 0
      FreeAPS/Sources/APS/APSManager.swift
  3. 0 2
      FreeAPS/Sources/APS/CGM/AppGroupSource.swift
  4. 1 26
      FreeAPS/Sources/APS/CGM/HeartBeatManager.swift
  5. 70 35
      FreeAPS/Sources/APS/DeviceDataManager.swift
  6. 1 1
      FreeAPS/Sources/APS/Extensions/PumpManagerExtensions.swift
  7. 3 3
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  8. 2 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  9. 103 0
      FreeAPS/Sources/APS/Storage/AlertStorage.swift
  10. 67 1
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  11. 1 0
      FreeAPS/Sources/Assemblies/StorageAssembly.swift
  12. 142 0
      FreeAPS/Sources/Models/AlertEntry.swift
  13. 5 0
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  14. 8 0
      FreeAPS/Sources/Modules/PumpConfig/PumpConfigProvider.swift
  15. 11 0
      FreeAPS/Sources/Modules/PumpConfig/PumpConfigStateModel.swift
  16. 4 0
      FreeAPS/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  17. 4 4
      FreeAPS/Sources/Modules/PumpConfig/View/PumpSetupView.swift
  18. 2 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  19. 3 3
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  20. 2 2
      FreeAPS/Sources/Services/Network/NetworkService.swift
  21. 4 0
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  22. 1 2
      FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift
  23. 4 5
      FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift
  24. 40 12
      README.md

+ 8 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -287,6 +287,8 @@
 		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
+		CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02428E867BA00473A9C /* AlertStorage.swift */; };
+		CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82E02628E869DF00473A9C /* AlertEntry.swift */; };
 		CEB434DC28B8F5B900B70274 /* MKRingProgressView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */; };
 		CEB434DD28B8F5B900B70274 /* MKRingProgressView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CEB434DF28B8F5C400B70274 /* OmniBLE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEB434DE28B8F5C400B70274 /* OmniBLE.framework */; };
@@ -716,6 +718,8 @@
 		C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusDataFlow.swift; sourceTree = "<group>"; };
 		CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBLEPumpManagerExtensions.swift; sourceTree = "<group>"; };
 		CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniPodManagerExtensions.swift; sourceTree = "<group>"; };
+		CE82E02428E867BA00473A9C /* AlertStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStorage.swift; sourceTree = "<group>"; };
+		CE82E02628E869DF00473A9C /* AlertEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertEntry.swift; sourceTree = "<group>"; };
 		CEB434DB28B8F5B900B70274 /* MKRingProgressView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MKRingProgressView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434DE28B8F5C400B70274 /* OmniBLE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmniBLE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		CEB434E228B8F9DB00B70274 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = "<group>"; };
@@ -1322,6 +1326,7 @@
 				E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */,
 				19F79FA8283AE7E000646323 /* TDD.swift */,
 				1935363F28496F7D001E0B16 /* TDD_averages.swift */,
+				CE82E02628E869DF00473A9C /* AlertEntry.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1362,6 +1367,7 @@
 				38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */,
 				38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
 				38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */,
+				CE82E02428E867BA00473A9C /* AlertStorage.swift */,
 			);
 			path = Storage;
 			sourceTree = "<group>";
@@ -2235,6 +2241,7 @@
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
+				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
 				3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */,
@@ -2329,6 +2336,7 @@
 				33E198D3039045D98C3DC5D4 /* AddCarbsStateModel.swift in Sources */,
 				28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */,
 				D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */,
+				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,

+ 1 - 0
FreeAPS/Sources/APS/APSManager.swift

@@ -62,6 +62,7 @@ final class BaseAPSManager: APSManager, Injectable {
     private let processQueue = DispatchQueue(label: "BaseAPSManager.processQueue")
     @Injected() private var storage: FileStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
+    @Injected() private var alertHistoryStorage: AlertHistoryStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var carbsStorage: CarbsStorage!

+ 0 - 2
FreeAPS/Sources/APS/CGM/AppGroupSource.swift

@@ -20,8 +20,6 @@ struct AppGroupSource: GlucoseSource {
             return []
         }
 
-        debug(.deviceManager, "in fetchLastBGs")
-
         HeartBeatManager.shared.checkCGMBluetoothTransmitter(sharedUserDefaults: sharedDefaults)
 
         let decoded = try? JSONSerialization.jsonObject(with: sharedData, options: [])

+ 1 - 26
FreeAPS/Sources/APS/CGM/HeartBeatManager.swift

@@ -23,15 +23,7 @@ class HeartBeatManager {
     /// - parameters:
     ///     - sharedData : shared User Defaults
     public func checkCGMBluetoothTransmitter(sharedUserDefaults: UserDefaults) {
-        if let sharedTransmitterAddress = sharedUserDefaults.string(forKey: keyForcgmTransmitterDeviceAddress) {
-            debug(.deviceManager, "in checkCGMBluetoothTransmitter, sharedTransmitterAddress = \(sharedTransmitterAddress)")
-        } else {
-            debug(.deviceManager, "in checkCGMBluetoothTransmitter, sharedTransmitterAddress = nil")
-        }
-
         if !initialSetupDone {
-            debug(.deviceManager, "in checkCGMBluetoothTransmitter, initial setup")
-
             initialSetupDone = true
 
             // set to nil, this will force recreation of bluetooth transmitter at app startup
@@ -41,16 +33,6 @@ class HeartBeatManager {
         if UserDefaults.standard.cgmTransmitterDeviceAddress != sharedUserDefaults
             .string(forKey: keyForcgmTransmitterDeviceAddress)
         {
-            debug(
-                .deviceManager,
-                "UserDefaults.standard.cgmTransmitterDeviceAddress != sharedUserDefaults.string(forKey: keyForcgmTransmitterDeviceAddress)"
-            )
-            if let sharedTransmitterAddress = sharedUserDefaults.string(forKey: keyForcgmTransmitterDeviceAddress) {
-                debug(.deviceManager, "in checkCGMBluetoothTransmitter, sharedTransmitterAddress = \(sharedTransmitterAddress)")
-            } else {
-                debug(.deviceManager, "in checkCGMBluetoothTransmitter, sharedTransmitterAddress = nil")
-            }
-
             // assign local copy of cgmTransmitterDeviceAddress to the value stored in sharedUserDefaults (possibly nil value)
             UserDefaults.standard.cgmTransmitterDeviceAddress = sharedUserDefaults
                 .string(forKey: keyForcgmTransmitterDeviceAddress)
@@ -76,22 +58,15 @@ class HeartBeatManager {
                     heartbeat: {}
                 )
 
-                debug(
-                    .deviceManager,
-                    "in setupBluetoothTransmitter, cgmTransmitterDeviceAddress in shared user defaults is not nil"
-                )
-
                 return newBluetoothTransmitter
 
             } else {
                 // looks like a coding error, xdrip4iOS did set a value for cgmTransmitterDeviceAddress in sharedUserDefaults but did not set a value for cgmTransmitter_CBUUID_Service or cgmTransmitter_CBUUID_Receive
-                debug(.deviceManager, "in setupBluetoothTransmitter, possible coding error")
+
                 return nil
             }
         }
 
-        debug(.deviceManager, "in setupBluetoothTransmitter, cgmTransmitterDeviceAddress in shared user defaults is nil")
-
         return nil
     }
 }

+ 70 - 35
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -24,7 +24,7 @@ protocol DeviceDataManager: GlucoseSource {
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     func heartbeat(date: Date)
     func createBolusProgressReporter() -> DoseProgressReporter?
-    var alertStore: [Alert] { get }
+    var alertHistoryStorage: AlertHistoryStorage! { get }
 }
 
 private let staticPumpManagers: [PumpManagerUI.Type] = [
@@ -50,6 +50,7 @@ private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLoc
 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!
@@ -65,7 +66,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     let errorSubject = PassthroughSubject<Error, Never>()
     let pumpNewStatus = PassthroughSubject<Void, Never>()
     let manualTempBasal = PassthroughSubject<Bool, Never>()
-    var alertStore: [Alert]
     private let router = FreeAPSApp.resolver.resolve(Router.self)!
     @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
     private var pumpUpdatePromise: Future<Bool, Never>.Promise?
@@ -113,10 +113,10 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
     let pumpName = CurrentValueSubject<String, Never>("Pump")
 
     init(resolver: Resolver) {
-        alertStore = []
         injectServices(resolver)
         setupPumpManager()
         UIDevice.current.isBatteryMonitoringEnabled = true
+        broadcaster.register(AlertObserver.self, observer: self)
     }
 
     func setupPumpManager() {
@@ -468,41 +468,22 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
 extension BaseDeviceDataManager: DeviceManagerDelegate {
     func issueAlert(_ alert: Alert) {
-        if !alertStore.contains(where: { $0.identifier.alertIdentifier == alert.identifier.alertIdentifier }) {
-            alertStore.append(alert)
-
-            let typeMessage: MessageType
-            let alertUp = alert.identifier.alertIdentifier.uppercased()
-            if alertUp.contains("FAULT") || alertUp.contains("ERROR") {
-                typeMessage = .errorPump
-            } else {
-                typeMessage = .warning
-            }
-
-            DispatchQueue.main.async {
-                let messageCont = MessageContent(content: alert.foregroundContent!.body, type: typeMessage)
-                self.router.alertMessage.send(messageCont)
-                // validation
-                self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.identifier.alertIdentifier) { error in
-                    if let error = error {
-                        debug(.deviceManager, "acknowledge not succeeded with error \(error.localizedDescription)")
-                    }
-                }
-            }
-
-            broadcaster.notify(pumpNotificationObserver.self, on: processQueue) {
-                $0.pumpNotification(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) {
-        if let idx = alertStore.firstIndex(where: { $0.identifier.alertIdentifier == identifier.alertIdentifier }) {
-            alertStore.remove(at: idx)
-            broadcaster.notify(pumpNotificationObserver.self, on: processQueue) {
-                $0.pumpRemoveNotification()
-            }
-        }
+        alertHistoryStorage.deleteAlert(identifier: identifier.alertIdentifier)
     }
 
     func doesIssuedAlertExist(identifier _: Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {
@@ -578,6 +559,60 @@ extension BaseDeviceDataManager: CGMManagerDelegate {
 
 // 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) {}

+ 1 - 1
FreeAPS/Sources/APS/Extensions/PumpManagerExtensions.swift

@@ -24,7 +24,7 @@ extension PumpManagerUI {
             bluetoothProvider: bluetoothProvider,
             colorPalette: .default,
             allowDebugFeatures: false,
-            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+            allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
         )
     }
 

+ 3 - 3
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -57,8 +57,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         timer.publisher
             .receive(on: processQueue)
             .flatMap { date -> AnyPublisher<(Date, Date, [BloodGlucose], [BloodGlucose]), Never> in
-                debug(.nightscout, "FetchGlucoseManager heartbeat")
-                debug(.nightscout, "Start fetching glucose")
+                // debug(.nightscout, "FetchGlucoseManager heartbeat")
+                // debug(.nightscout, "Start fetching glucose")
                 self.updateGlucoseSource()
                 return Publishers.CombineLatest4(
                     Just(date),
@@ -69,7 +69,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                 .eraseToAnyPublisher()
             }
             .sink { date, syncDate, glucose, glucoseFromHealth in
-                debug(.nightscout, "SyncDate is \(syncDate)")
+                //  debug(.nightscout, "SyncDate is \(syncDate)")
                 let allGlucose = glucose + glucoseFromHealth
                 guard allGlucose.isNotEmpty else { return }
 

+ 2 - 0
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -56,6 +56,7 @@ extension OpenAPS {
         static let podAge = "monitor/pod-age.json"
         static let tdd = "monitor/tdd.json"
         static let tdd_averages = "monitor/tdd_averages.json"
+        static let alertHistory = "monitor/alerthistory.json"
     }
 
     enum Enact {
@@ -84,6 +85,7 @@ extension OpenAPS {
         static let uploadedCGMState = "upload/uploaded-cgm-state.json"
         static let uploadedPodAge = "upload/uploaded-pod-age.json"
         static let uploadedProfile = "upload/uploaded-profile.json"
+        static let uploadedPodAge = "upload/uploaded-pod-age.json"
     }
 
     enum FreeAPS {

+ 103 - 0
FreeAPS/Sources/APS/Storage/AlertStorage.swift

@@ -0,0 +1,103 @@
+import Combine
+import Foundation
+import SwiftDate
+import Swinject
+
+protocol AlertObserver {
+    func AlertDidUpdate(_ alerts: [AlertEntry])
+}
+
+protocol AlertHistoryStorage {
+    func storeAlert(_ alerts: AlertEntry)
+    func syncDate() -> Date
+    func recentNotAck() -> [AlertEntry]
+    func deleteAlert(identifier: String)
+    func ackAlert(_ alert: Date, _ error: String?)
+    func forceNotification()
+    var alertNotAck: PassthroughSubject<Bool, Never> { get }
+}
+
+final class BaseAlertHistoryStorage: AlertHistoryStorage, Injectable {
+    private let processQueue = DispatchQueue.markedQueue(label: "BaseAlertsStorage.processQueue")
+    @Injected() private var storage: FileStorage!
+    @Injected() private var broadcaster: Broadcaster!
+
+    let alertNotAck = PassthroughSubject<Bool, Never>()
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        alertNotAck.send(recentNotAck().isNotEmpty)
+    }
+
+    func storeAlert(_ alert: AlertEntry) {
+        processQueue.sync {
+            let file = OpenAPS.Monitor.alertHistory
+            var uniqEvents: [AlertEntry] = []
+            self.storage.transaction { storage in
+                storage.append(alert, to: file, uniqBy: \.issuedDate)
+                uniqEvents = storage.retrieve(file, as: [AlertEntry].self)?
+                    .filter { $0.issuedDate.addingTimeInterval(1.days.timeInterval) > Date() }
+                    .sorted { $0.issuedDate > $1.issuedDate } ?? []
+                storage.save(Array(uniqEvents), as: file)
+            }
+            alertNotAck.send(self.recentNotAck().isNotEmpty)
+            broadcaster.notify(AlertObserver.self, on: processQueue) {
+                $0.AlertDidUpdate(uniqEvents)
+            }
+        }
+    }
+
+    func syncDate() -> Date {
+        Date().addingTimeInterval(-1.days.timeInterval)
+    }
+
+    func recentNotAck() -> [AlertEntry] {
+        storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self)?
+            .filter { $0.issuedDate.addingTimeInterval(1.days.timeInterval) > Date() && $0.acknowledgedDate == nil }
+            .sorted { $0.issuedDate > $1.issuedDate } ?? []
+    }
+
+    func ackAlert(_ alert: Date, _ error: String?) {
+        processQueue.sync {
+            var allValues = storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self) ?? []
+            guard let entryIndex = allValues.firstIndex(where: { $0.issuedDate == alert }) else {
+                return
+            }
+
+            if let error {
+                allValues[entryIndex].errorMessage = error
+            } else {
+                allValues[entryIndex].acknowledgedDate = Date()
+            }
+            storage.save(allValues, as: OpenAPS.Monitor.alertHistory)
+            alertNotAck.send(self.recentNotAck().isNotEmpty)
+        }
+    }
+
+    func deleteAlert(identifier: String) {
+        processQueue.sync {
+            var allValues = storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self) ?? []
+            guard let entryIndex = allValues.firstIndex(where: { $0.alertIdentifier == identifier }) else {
+                return
+            }
+            allValues.remove(at: entryIndex)
+            storage.save(allValues, as: OpenAPS.Monitor.alertHistory)
+            alertNotAck.send(self.recentNotAck().isNotEmpty)
+            broadcaster.notify(AlertObserver.self, on: processQueue) {
+                $0.AlertDidUpdate(allValues)
+            }
+        }
+    }
+
+    func forceNotification() {
+        processQueue.sync {
+            let uniqEvents = storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self)?
+                .filter { $0.issuedDate.addingTimeInterval(1.days.timeInterval) > Date() }
+                .sorted { $0.issuedDate > $1.issuedDate } ?? []
+            alertNotAck.send(self.recentNotAck().isNotEmpty)
+            broadcaster.notify(AlertObserver.self, on: processQueue) {
+                $0.AlertDidUpdate(uniqEvents)
+            }
+        }
+    }
+}

+ 67 - 1
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -136,6 +136,15 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             carbInput: nil
                         )
                     ]
+                case .alarm:
+                    return [
+                        PumpHistoryEvent(
+                            id: id,
+                            type: .pumpAlarm,
+                            timestamp: event.date,
+                            note: event.title
+                        )
+                    ]
                 default:
                     return []
                 }
@@ -260,9 +269,66 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             }
         }
 
+        let misc = events.compactMap { event -> NigtscoutTreatment? in
+            switch event.type {
+            case .prime:
+                return NigtscoutTreatment(
+                    duration: event.duration,
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .nsSiteChange,
+                    createdAt: event.timestamp,
+                    enteredBy: NigtscoutTreatment.local,
+                    bolus: event,
+                    insulin: nil,
+                    notes: nil,
+                    carbs: nil,
+                    targetTop: nil,
+                    targetBottom: nil
+                )
+            case .rewind:
+                return NigtscoutTreatment(
+                    duration: nil,
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .nsInsulinChange,
+                    createdAt: event.timestamp,
+                    enteredBy: NigtscoutTreatment.local,
+                    bolus: nil,
+                    insulin: nil,
+                    notes: nil,
+                    carbs: nil,
+                    targetTop: nil,
+                    targetBottom: nil
+                )
+            case .pumpAlarm:
+                return NigtscoutTreatment(
+                    duration: 30, // minutes
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .nsAnnouncement,
+                    createdAt: event.timestamp,
+                    enteredBy: NigtscoutTreatment.local,
+                    bolus: nil,
+                    insulin: nil,
+                    notes: "Alarm \(String(describing: event.note)) \(event.type)",
+                    carbs: nil,
+                    targetTop: nil,
+                    targetBottom: nil
+                )
+            default: return nil
+            }
+        }
+
         let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
 
-        let treatments = Array(Set([bolusesAndCarbs, temps].flatMap { $0 }).subtracting(Set(uploaded)))
+        let treatments = Array(Set([bolusesAndCarbs, temps, misc].flatMap { $0 }).subtracting(Set(uploaded)))
 
         return treatments.sorted { $0.createdAt! > $1.createdAt! }
     }

+ 1 - 0
FreeAPS/Sources/Assemblies/StorageAssembly.swift

@@ -14,5 +14,6 @@ final class StorageAssembly: Assembly {
         container.register(AnnouncementsStorage.self) { r in BaseAnnouncementsStorage(resolver: r) }
         container.register(SettingsManager.self) { r in BaseSettingsManager(resolver: r) }
         container.register(Keychain.self) { _ in BaseKeychain() }
+        container.register(AlertHistoryStorage.self) { r in BaseAlertHistoryStorage(resolver: r) }
     }
 }

+ 142 - 0
FreeAPS/Sources/Models/AlertEntry.swift

@@ -0,0 +1,142 @@
+
+import Foundation
+import LoopKit
+import UserNotifications
+
+struct AlertEntry: JSON, Codable, Hashable {
+    let alertIdentifier: String
+    var acknowledgedDate: Date?
+    var primitiveInterruptionLevel: Decimal?
+    let issuedDate: Date
+    let managerIdentifier: String
+    let triggerType: Int16
+    var triggerInterval: Decimal?
+    let contentTitle: String?
+    let contentBody: String?
+    var errorMessage: String?
+
+    static let manual = "freeaps-x"
+
+    static func == (lhs: AlertEntry, rhs: AlertEntry) -> Bool {
+        lhs.issuedDate == rhs.issuedDate
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(issuedDate)
+    }
+
+    private enum CodingKeys: String, CodingKey {
+        case alertIdentifier
+        case acknowledgedDate
+        case primitiveInterruptionLevel
+        case issuedDate
+        case managerIdentifier
+        case triggerType
+        case triggerInterval
+        case contentTitle
+        case contentBody
+        case errorMessage
+    }
+}
+
+//
+//  StoredAlert.swift
+//  Loop
+//
+//  Created by Rick Pasetto on 5/11/20.
+//  Copyright © 2020 LoopKit Authors. All rights reserved.
+//
+
+extension Alert.Trigger {
+    enum StorageError: Error {
+        case invalidStoredInterval
+        case invalidStoredType
+    }
+
+    var storedType: Int16 {
+        switch self {
+        case .immediate: return 0
+        case .delayed: return 1
+        case .repeating: return 2
+        }
+    }
+
+    var storedInterval: NSNumber? {
+        switch self {
+        case .immediate: return nil
+        case let .delayed(interval): return NSNumber(value: interval)
+        case let .repeating(repeatInterval): return NSNumber(value: repeatInterval)
+        }
+    }
+
+    init(storedType: Int16, storedInterval: NSNumber?, storageDate: Date? = nil, now: Date = Date()) throws {
+        switch storedType {
+        case 0: self = .immediate
+        case 1:
+            if let storedInterval = storedInterval {
+                if let storageDate = storageDate, storageDate <= now {
+                    let intervalLeft = storedInterval.doubleValue - now.timeIntervalSince(storageDate)
+                    if intervalLeft <= 0 {
+                        self = .immediate
+                    } else {
+                        self = .delayed(interval: intervalLeft)
+                    }
+                } else {
+                    self = .delayed(interval: storedInterval.doubleValue)
+                }
+            } else {
+                throw StorageError.invalidStoredInterval
+            }
+        case 2:
+            // Strange case here: if it is a repeating trigger, we can't really play back exactly
+            // at the right "remaining time" and then repeat at the original period.  So, I think
+            // the best we can do is just use the original trigger
+            if let storedInterval = storedInterval {
+                self = .repeating(repeatInterval: storedInterval.doubleValue)
+            } else {
+                throw StorageError.invalidStoredInterval
+            }
+        default:
+            throw StorageError.invalidStoredType
+        }
+    }
+}
+
+extension Alert.InterruptionLevel {
+    var storedValue: NSNumber {
+        // Since this is arbitrary anyway, might as well make it match iOS's values
+        switch self {
+        case .active:
+            if #available(iOS 15.0, *) {
+                return NSNumber(value: UNNotificationInterruptionLevel.active.rawValue)
+            } else {
+                // https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel/active
+                return 1
+            }
+        case .timeSensitive:
+            if #available(iOS 15.0, *) {
+                return NSNumber(value: UNNotificationInterruptionLevel.timeSensitive.rawValue)
+            } else {
+                // https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel/timesensitive
+                return 2
+            }
+        case .critical:
+            if #available(iOS 15.0, *) {
+                return NSNumber(value: UNNotificationInterruptionLevel.critical.rawValue)
+            } else {
+                // https://developer.apple.com/documentation/usernotifications/unnotificationinterruptionlevel/critical
+                return 3
+            }
+        }
+    }
+
+    init?(storedValue: NSNumber) {
+        switch storedValue {
+        case Self.active.storedValue: self = .active
+        case Self.timeSensitive.storedValue: self = .timeSensitive
+        case Self.critical.storedValue: self = .critical
+        default:
+            return nil
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -47,6 +47,8 @@ enum EventType: String, JSON {
     case tempBasalDuration = "TempBasalDuration"
     case pumpSuspend = "PumpSuspend"
     case pumpResume = "PumpResume"
+    case pumpAlarm = "PumpAlarm"
+    case pumpBattery = "PumpBattery"
     case rewind = "Rewind"
     case prime = "Prime"
     case journalCarbs = "JournalEntryMealMarker"
@@ -54,7 +56,10 @@ enum EventType: String, JSON {
     case nsTempBasal = "Temp Basal"
     case nsCarbCorrection = "Carb Correction"
     case nsTempTarget = "Temporary Target"
+    case nsInsulinChange = "Insulin Change"
     case nsSiteChange = "Site Change"
+    case nsBatteryChange = "Pump Battery Change"
+    case nsAnnouncement = "Announcement"
     case nsSensorChange = "Sensor Start"
 }
 

+ 8 - 0
FreeAPS/Sources/Modules/PumpConfig/PumpConfigProvider.swift

@@ -24,5 +24,13 @@ extension PumpConfig {
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
                 ?? PumpSettings(insulinActionCurve: 5, maxBolus: 10, maxBasal: 2)
         }
+
+        var alertNotAck: AnyPublisher<Bool, Never> {
+            deviceManager.alertHistoryStorage.alertNotAck.eraseToAnyPublisher()
+        }
+
+        func initialAlertNotAck() -> Bool {
+            deviceManager.alertHistoryStorage.recentNotAck().isNotEmpty
+        }
     }
 }

+ 11 - 0
FreeAPS/Sources/Modules/PumpConfig/PumpConfigStateModel.swift

@@ -9,6 +9,7 @@ extension PumpConfig {
         private(set) var setupPumpType: PumpType = .minimed
         @Published var pumpState: PumpDisplayState?
         private(set) var initialSettings: PumpInitialSettings = .default
+        @Published var alertNotAck: Bool = false
 
         override func subscribe() {
             provider.pumpDisplayState
@@ -16,6 +17,12 @@ extension PumpConfig {
                 .assign(to: \.pumpState, on: self)
                 .store(in: &lifetime)
 
+            alertNotAck = provider.initialAlertNotAck()
+            provider.alertNotAck
+                .receive(on: DispatchQueue.main)
+                .assign(to: \.alertNotAck, on: self)
+                .store(in: &lifetime)
+
             let basalSchedule = BasalRateSchedule(
                 dailyItems: provider.basalProfile().map {
                     RepeatingScheduleValue(startTime: $0.minutes.minutes.timeInterval, value: Double($0.rate))
@@ -35,6 +42,10 @@ extension PumpConfig {
             setupPump = true
             setupPumpType = type
         }
+
+        func ack() {
+            provider.deviceManager.alertHistoryStorage.forceNotification()
+        }
     }
 }
 

+ 4 - 0
FreeAPS/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -19,6 +19,10 @@ extension PumpConfig {
                                     Text(pumpState.name)
                                 }
                             }
+                            if state.alertNotAck {
+                                Spacer()
+                                Button("Acknowledge all alerts") { state.ack() }
+                            }
                         } else {
                             Button("Add Medtronic") { state.addPump(.minimed) }
                             Button("Add Omnipod") { state.addPump(.omnipod) }

+ 4 - 4
FreeAPS/Sources/Modules/PumpConfig/View/PumpSetupView.swift

@@ -38,7 +38,7 @@ extension PumpConfig {
                     bluetoothProvider: bluetoothManager,
                     colorPalette: .default,
                     allowDebugFeatures: false,
-                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                 )
             case .omnipod:
                 setupViewController = OmnipodPumpManager.setupViewController(
@@ -46,7 +46,7 @@ extension PumpConfig {
                     bluetoothProvider: bluetoothManager,
                     colorPalette: .default,
                     allowDebugFeatures: false,
-                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                 )
             case .omnipodBLE:
                 setupViewController = OmniBLEPumpManager.setupViewController(
@@ -54,7 +54,7 @@ extension PumpConfig {
                     bluetoothProvider: bluetoothManager,
                     colorPalette: .default,
                     allowDebugFeatures: false,
-                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                 )
             case .simulator:
                 setupViewController = MockPumpManager.setupViewController(
@@ -62,7 +62,7 @@ extension PumpConfig {
                     bluetoothProvider: bluetoothManager,
                     colorPalette: .default,
                     allowDebugFeatures: false,
-                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev, .afrezza]
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                 )
             }
 

+ 2 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -100,6 +100,8 @@ extension Settings {
                                 .navigationLink(to: .configEditor(file: OpenAPS.Nightscout.uploadedCGMState), from: self)
                         }
                         Group {
+                            Text("Alerts")
+                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.alertHistory), from: self)
                             Text("Target presets")
                                 .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self)
                             Text("Calibrations")

+ 3 - 3
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -295,7 +295,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             }
 
             self.processQueue.async {
-                debug(.service, "Start fetching HealthKitManager")
+                //   debug(.service, "Start fetching HealthKitManager")
                 guard self.settingsManager.settings.useAppleHealth else {
                     debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
                     promise(.success([]))
@@ -312,9 +312,9 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                 self.newGlucose = self.newGlucose
                     .filter { !actualGlucose.contains($0) }
 
-                debug(.service, "Actual glucose is \(actualGlucose)")
+                //  debug(.service, "Actual glucose is \(actualGlucose)")
 
-                debug(.service, "Current state of newGlucose is \(self.newGlucose)")
+                //  debug(.service, "Current state of newGlucose is \(self.newGlucose)")
 
                 promise(.success(actualGlucose))
             }

+ 2 - 2
FreeAPS/Sources/Services/Network/NetworkService.swift

@@ -14,8 +14,8 @@ enum NetworkError: Error, LocalizedError {
 
 struct NetworkService {
     func run(_ request: URLRequest) -> AnyPublisher<Data, Error> {
-        debug(.nightscout, "\(request.httpMethod!)  ***\(request.url!.path)\(request.url!.query.map { "?" + $0 } ?? "")")
-        return URLSession.shared
+        //    debug(.nightscout, "\(request.httpMethod!)  ***\(request.url!.path)\(request.url!.query.map { "?" + $0 } ?? "")")
+        URLSession.shared
             .dataTaskPublisher(for: request)
             .tryMap { data, response in
                 let code = (response as! HTTPURLResponse).statusCode

+ 4 - 0
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -239,6 +239,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 .store(in: &self.lifetime)
         }
 
+        uploadPodAge()
+    }
+
+    func uploadPodAge() {
         let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NigtscoutTreatment].self) ?? []
         if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
            uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!

+ 1 - 2
FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift

@@ -64,8 +64,7 @@ final class BaseSettingsManager: SettingsManager, Injectable {
              .novolog:
             prefs.curve = .rapidActing
 
-        case .afrezza,
-             .fiasp,
+        case .fiasp,
              .lyumjev:
             prefs.curve = .ultraRapid
         default:

+ 4 - 5
FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

@@ -25,7 +25,7 @@ protocol BolusFailureObserver {
 }
 
 protocol pumpNotificationObserver {
-    func pumpNotification(alert: LoopKit.Alert)
+    func pumpNotification(alert: AlertEntry)
     func pumpRemoveNotification()
 }
 
@@ -405,12 +405,11 @@ extension BaseUserNotificationsManager: GlucoseObserver {
 }
 
 extension BaseUserNotificationsManager: pumpNotificationObserver {
-    func pumpNotification(alert: LoopKit.Alert) {
+    func pumpNotification(alert: AlertEntry) {
         ensureCanSendNotification {
-            let contentAlert = alert.foregroundContent!
             let content = UNMutableNotificationContent()
-            content.title = contentAlert.title
-            content.body = contentAlert.body
+            content.title = alert.contentTitle ?? "Unknown"
+            content.body = alert.contentBody ?? "Unknown"
             content.sound = .default
             self.addRequest(
                 identifier: .pumpNotification,

+ 40 - 12
README.md

@@ -1,17 +1,54 @@
 # FreeAPS X
 
+## Introduction 
+
 FreeAPS X - an artificial pancreas system for iOS based on [OpenAPS Reference](https://github.com/openaps/oref0) algorithms
 
-FreeAPS X uses original JavaScript files of oref0 and provides a user interface (UI) to control and set up the system
+FreeAPS X uses original JavaScript files of oref0 and provides a user interface (UI) to control and set up the system. 
+
+This repo includes two branchs allowing to use OmniPod Dash pumps : 
+- the branch dash_dev includes the dash pump in the setting pump 
+- the branch dash_garmin_disf_dev includes the dash pump, but also the dISF implementation (with update of openAPS) and the garmin service to connect with garmin watches. 
+
+To use this branch : 
+
+git clone -b dash_dev remote-repo-url or git clone -b dash_garmin_disf_dev remote-repo-url 
+
+or use directly Xcode to use one specific branch. 
+
+Don't forget to copy / reference your ConfigOverride 
+
+:warning: :warning: :warning: :warning:
+
+# Precaution 
+
+Please understand that these version are :
+- highly experimental
+- not approved for therapy
+
+WARNING 
+- The settings of your current FAX should not be re-init when you update to this version but check it before close loop 
+- The update MUST ONLY be done when you change of a pod. The previous pod would be not accessible. So, first, desactivate your current pod then compile and update your FAX on your phone and add a new pod with the dash pump menu.
 
-## Documentation
+
+These version were tested by few developers with success. But...Don't hesitate to create issues if you find bugs or issues. 
+
+:warning: :warning: :warning: 
+
+
+# Documentation
 
 [freeAPS X original github](https://github.com/ivalkou/freeaps)
 
+[ADD DASH PUMP and SETTINGS](https://loopkit.github.io/loopdocs/loop-3/omnipod/)
+
 [Overview & Onboarding Tips on Loop&Learn](https://www.loopandlearn.org/freeaps-x/)
 
 [OpenAPS documentation](https://openaps.readthedocs.io/en/latest/)
 
+
+# Technical updates 
+
 ## Updated to include dashpod
 
 - replace the Rileylink package to the Loop version of 2 august 2022
@@ -22,16 +59,7 @@ _ modify the order of compilation for CGMBLEKit (header before compilation)
  
  ## Changes in package 
  
- The only change required is the public access to managedIdentifier for omnipod, medtronic et  dash. Loop doesn't use it but FAX requires it. 
-
-
-    //public let managerIdentifier: String = "Omnipod-Dash" // use a single token to make parsing log files easier
-    
-    public static let managerIdentifier = "Omnipod-Dash"
-    
-    public var managerIdentifier: String {
-        return OmniBLEPumpManager.managerIdentifier
-    }
+No change 😁. Use extension in FAX to include the managerIdentifier
 
  
  ## Changes in freeapsx