Bladeren bron

Announcements

Ivan Valkou 5 jaren geleden
bovenliggende
commit
405fd07198

+ 10 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -93,6 +93,8 @@
 		384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803325C385E60086DB71 /* JavaScriptWorker.swift */; };
 		384E803825C388640086DB71 /* Script.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803725C388640086DB71 /* Script.swift */; };
 		385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */; };
+		385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC025F2EA52002D6D5B /* Announcement.swift */; };
+		385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */; };
 		386ED25E25EE48B500820A49 /* MockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17ADB25DD6A40005CAE3D /* MockKit.framework */; };
 		386ED25F25EE48B500820A49 /* MockKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17ADB25DD6A40005CAE3D /* MockKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		386ED26025EE48B500820A49 /* MockKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38B17ADF25DD6A40005CAE3D /* MockKitUI.framework */; };
@@ -685,6 +687,8 @@
 		384E803325C385E60086DB71 /* JavaScriptWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptWorker.swift; sourceTree = "<group>"; };
 		384E803725C388640086DB71 /* Script.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Script.swift; sourceTree = "<group>"; };
 		385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStatus.swift; sourceTree = "<group>"; };
+		385CEAC025F2EA52002D6D5B /* Announcement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = "<group>"; };
+		385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsStorage.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
 		3871F38625ED661C0013ECB5 /* Suggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = "<group>"; };
 		3871F39B25ED892B0013ECB5 /* TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTarget.swift; sourceTree = "<group>"; };
@@ -1239,6 +1243,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
 				388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */,
 				38D0B3B525EBE24900CB6E88 /* Battery.swift */,
@@ -1283,10 +1288,11 @@
 		38A0362725ECF05300FCBB52 /* Storage */ = {
 			isa = PBXGroup;
 			children = (
-				38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
+				385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */,
+				38AEE75625F0F18E0013F05B /* CarbsStorage.swift */,
 				38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */,
+				38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */,
 				38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */,
-				38AEE75625F0F18E0013F05B /* CarbsStorage.swift */,
 			);
 			path = Storage;
 			sourceTree = "<group>";
@@ -2002,6 +2008,7 @@
 				3811DE4C25C9D4B800A708ED /* AuthotizedRootBuilder.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
+				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
@@ -2131,6 +2138,7 @@
 				6B9625766B697D1C98E455A2 /* PumpSettingsEditorViewModel.swift in Sources */,
 				A0B8EC8CC5CD1DD237D1BCD2 /* PumpSettingsEditorRootView.swift in Sources */,
 				1D086541F369D339A74893AC /* BasalProfileEditorBuilder.swift in Sources */,
+				385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */,
 				8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorViewModel.swift in Sources */,

+ 1 - 0
FreeAPS/Resources/json/defaults/freeaps/announcements.json

@@ -0,0 +1 @@
+[]

FreeAPS/Resources/json/defaults/freeaps_settings.json → FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json


+ 54 - 4
FreeAPS/Sources/APS/APSManager.swift

@@ -5,7 +5,7 @@ import LoopKitUI
 import Swinject
 
 protocol APSManager {
-    func loop()
+    func fetchAndLoop()
     func autosense()
     func autotune()
     var pumpManager: PumpManagerUI? { get set }
@@ -18,6 +18,7 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
+    @Injected() private var announcementsStorage: AnnouncementsStorage!
     @Injected() private var deviceDataManager: DeviceDataManager!
     @Injected() private var nightscout: NightscoutManager!
     @Injected() private var settingsManager: SettingsManager!
@@ -26,6 +27,7 @@ final class BaseAPSManager: APSManager, Injectable {
     private var loopCancellable: AnyCancellable?
     private var pumpCancellable: AnyCancellable?
     private var enactCancellable: AnyCancellable?
+    private var remoteCancellable: AnyCancellable?
 
     var pumpManager: PumpManagerUI? {
         get { deviceDataManager.pumpManager }
@@ -50,11 +52,22 @@ final class BaseAPSManager: APSManager, Injectable {
     private func subscribe() {
         pumpCancellable = deviceDataManager.recommendsLoop
             .sink { [weak self] in
-                self?.loop()
+                self?.fetchAndLoop()
             }
     }
 
-    func loop() {
+    func fetchAndLoop() {
+        remoteCancellable = nightscout.fetchAnnouncements()
+            .sink { [weak self] in
+                if let recent = self?.announcementsStorage.recent(), recent.action != nil {
+                    self?.enactAnnouncement(recent)
+                } else {
+                    self?.loop()
+                }
+            }
+    }
+
+    private func loop() {
         loopCancellable = Publishers.CombineLatest3(
             nightscout.fetchGlucose(),
             nightscout.fetchCarbs(),
@@ -69,7 +82,7 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    func determineBasal() -> AnyPublisher<Bool, Never> {
+    private func determineBasal() -> AnyPublisher<Bool, Never> {
         guard let glucose = try? storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
             print("Not enough glucose data")
             return Just(false).eraseToAnyPublisher()
@@ -96,6 +109,43 @@ final class BaseAPSManager: APSManager, Injectable {
         _ = openAPS.autotune()
     }
 
+    private func enactAnnouncement(_ announcement: Announcement) {
+        guard let action = announcement.action else {
+            print("Invalid Announcement action")
+            return
+        }
+        switch action {
+        case let .bolus(amount):
+            pumpManager?.enactBolus(units: Double(amount), automatic: false) { result in
+                switch result {
+                case .success:
+                    print("Announcement Bolus succeeded")
+                case let .failure(error):
+                    print("Announcement Bolus failed with error: \(error.localizedDescription)")
+                }
+            }
+        case let .pump(pumpAction):
+            switch pumpAction {
+            case .suspend:
+                pumpManager?.suspendDelivery { error in
+                    if let error = error {
+                        print("Pump not suspended by Announcement: \(error.localizedDescription)")
+                    } else {
+                        print("Pump suspended by Announcement")
+                    }
+                }
+            case .resume:
+                pumpManager?.resumeDelivery { error in
+                    if let error = error {
+                        print("Pump not resumed by Announcement: \(error.localizedDescription)")
+                    } else {
+                        print("Pump resumed by Announcement")
+                    }
+                }
+            }
+        }
+    }
+
     private func currentTemp(date: Date) -> TempBasal? {
         guard let state = pumpManager?.status.basalDeliveryState else { return nil }
         switch state {

+ 6 - 1
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -22,7 +22,6 @@ extension OpenAPS {
     }
 
     enum Settings {
-        static let freeAPSSettings = "freeaps_settings.json"
         static let preferences = "preferences.json"
         static let autotune = "settings/autotune.json"
         static let autosense = "settings/autosense.json"
@@ -67,4 +66,10 @@ extension OpenAPS {
         static let tempBasalFunctions = "tempBasalFunctions"
         static let exportDefaults = "exportDefaults"
     }
+
+    enum FreeAPS {
+        static let settings = "freeaps/freeaps_settings.json"
+        static let announcements = "freeaps/announcements.json"
+        static let announcementsEnacted = "freeaps/announcements_enacted.json"
+    }
 }

+ 54 - 0
FreeAPS/Sources/APS/Storage/AnnouncementsStorage.swift

@@ -0,0 +1,54 @@
+import Foundation
+import SwiftDate
+import Swinject
+
+protocol AnnouncementsStorage {
+    func storeAnnouncements(_ announcements: [Announcement], enacted: Bool)
+    func syncDate() -> Date
+    func recent() -> Announcement?
+}
+
+final class BaseAnnouncementsStorage: AnnouncementsStorage, Injectable {
+    private let processQueue = DispatchQueue(label: "BaseAnnouncementsStorage.processQueue")
+    @Injected() private var storage: FileStorage!
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+    }
+
+    func storeAnnouncements(_ announcements: [Announcement], enacted: Bool) {
+        processQueue.sync {
+            let file = enacted ? OpenAPS.FreeAPS.announcementsEnacted : OpenAPS.FreeAPS.announcements
+            try? self.storage.transaction { storage in
+                try storage.append(announcements, to: file, uniqBy: \.createdAt)
+                let uniqEvents = try storage.retrieve(file, as: [Announcement].self)
+                    .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
+                    .sorted { $0.createdAt > $1.createdAt }
+                try storage.save(Array(uniqEvents), as: file)
+            }
+        }
+    }
+
+    func syncDate() -> Date {
+        guard let events = try? storage.retrieve(OpenAPS.FreeAPS.announcements, as: [Announcement].self),
+              let recent = events.filter({ $0.enteredBy != Announcement.remote }).first
+        else {
+            return Date().addingTimeInterval(-1.days.timeInterval)
+        }
+        return recent.createdAt.addingTimeInterval(-6.minutes.timeInterval)
+    }
+
+    func recent() -> Announcement? {
+        guard let events = try? storage.retrieve(OpenAPS.FreeAPS.announcements, as: [Announcement].self) else { return nil }
+        guard let recent = events
+            .filter({ $0.enteredBy != Announcement.remote && $0.createdAt.addingTimeInterval(10.minutes.timeInterval) > Date() })
+            .first else { return nil }
+        guard let enactedEvents = try? storage.retrieve(OpenAPS.FreeAPS.announcementsEnacted, as: [Announcement].self)
+        else { return recent }
+
+        guard enactedEvents.first(where: { $0.createdAt == recent.createdAt }) == nil else {
+            return nil
+        }
+        return recent
+    }
+}

+ 2 - 1
FreeAPS/Sources/Containers/StorageContainer.swift

@@ -13,7 +13,8 @@ enum StorageContainer: DependeciesContainer {
         container.register(GlucoseStorage.self) { _ in BaseGlucoseStorage(resolver: resolver) }
         container.register(TempTargetsStorage.self) { _ in BaseTempTargetsStorage(resolver: resolver) }
         container.register(CarbsStorage.self) { _ in BaseCarbsStorage(resolver: resolver) }
-        container.register(SettingsManager.self) { _ in BaseFSettingsManager(resolver: resolver) }
+        container.register(AnnouncementsStorage.self) { _ in BaseAnnouncementsStorage(resolver: resolver) }
+        container.register(SettingsManager.self) { _ in BaseSettingsManager(resolver: resolver) }
 
         container.register(Keychain.self) { _ in BaseKeychain() }
     }

+ 44 - 0
FreeAPS/Sources/Models/Announcement.swift

@@ -0,0 +1,44 @@
+import Foundation
+
+struct Announcement: JSON {
+    let createdAt: Date
+    let enteredBy: String
+    let notes: String
+
+    static let remote = "freeaps-x-remote"
+
+    var action: AnnouncementAction? {
+        let components = notes.replacingOccurrences(of: " ", with: "").split(separator: ":")
+        guard components.count == 2 else {
+            return nil
+        }
+
+        switch String(components[0]) {
+        case "bolus":
+            guard let amount = Decimal(from: String(components[1])) else { return nil }
+            return .bolus(amount)
+        case "pump":
+            guard let action = PumpAction(rawValue: String(components[1])) else { return nil }
+            return .pump(action)
+        default: return nil
+        }
+    }
+}
+
+extension Announcement {
+    private enum CodingKeys: String, CodingKey {
+        case createdAt = "created_at"
+        case enteredBy
+        case notes
+    }
+}
+
+enum AnnouncementAction {
+    case bolus(Decimal)
+    case pump(PumpAction)
+}
+
+enum PumpAction: String {
+    case suspend
+    case resume
+}

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -11,7 +11,7 @@ extension Home {
         }
 
         func runLoop() {
-            apsManager.loop()
+            apsManager.fetchAndLoop()
         }
 
         func addTempTarget() {

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

@@ -56,6 +56,10 @@ extension Settings {
                             .modal(for: .configEditor(file: OpenAPS.Enact.suggested), from: self)
                         Text("Enacted").chevronCell()
                             .modal(for: .configEditor(file: OpenAPS.Enact.enacted), from: self)
+                        Text("Announcements").chevronCell()
+                            .modal(for: .configEditor(file: OpenAPS.FreeAPS.announcements), from: self)
+                        Text("Enacted announcements").chevronCell()
+                            .modal(for: .configEditor(file: OpenAPS.FreeAPS.announcementsEnacted), from: self)
                     }
                 }
             }

+ 32 - 0
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -146,6 +146,38 @@ extension NightscoutAPI {
             .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
             .eraseToAnyPublisher()
     }
+
+    func fetchAnnouncement(sinceDate: Date? = nil) -> AnyPublisher<[Announcement], Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+        components.queryItems = [
+            URLQueryItem(name: "find[eventType]", value: "Announcement"),
+            URLQueryItem(name: "find[enteredBy]", value: Announcement.remote)
+        ]
+        if let date = sinceDate {
+            let dateItem = URLQueryItem(
+                name: "find[created_at][$gte]",
+                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+            )
+            components.queryItems?.append(dateItem)
+        }
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = Config.timeout
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+
+        return service.run(request)
+            .retry(Config.retryCount)
+            .decode(type: [Announcement].self, decoder: JSONCoding.decoder)
+            .eraseToAnyPublisher()
+    }
 }
 
 private extension String {

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

@@ -6,6 +6,7 @@ protocol NightscoutManager {
     func fetchGlucose() -> AnyPublisher<Void, Never>
     func fetchCarbs() -> AnyPublisher<Void, Never>
     func fetchTempTargets() -> AnyPublisher<Void, Never>
+    func fetchAnnouncements() -> AnyPublisher<Void, Never>
     func upload()
 }
 
@@ -14,6 +15,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
     @Injected() private var carbsStorage: CarbsStorage!
+    @Injected() private var announcementsStorage: AnnouncementsStorage!
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
 
@@ -74,6 +76,20 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }.eraseToAnyPublisher()
     }
 
+    func fetchAnnouncements() -> AnyPublisher<Void, Never> {
+        guard let nightscout = nightscoutAPI else {
+            return Just(()).eraseToAnyPublisher()
+        }
+
+        let since = announcementsStorage.syncDate()
+        return nightscout.fetchAnnouncement(sinceDate: since)
+            .replaceError(with: [])
+            .map {
+                self.announcementsStorage.storeAnnouncements($0, enacted: false)
+                return ()
+            }.eraseToAnyPublisher()
+    }
+
     func upload() {}
 
     private func uploadStatus() {}

+ 4 - 4
FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift

@@ -5,7 +5,7 @@ protocol SettingsManager {
     var settings: FreeAPSSettings { get set }
 }
 
-final class BaseFSettingsManager: SettingsManager, Injectable {
+final class BaseSettingsManager: SettingsManager, Injectable {
     var settings: FreeAPSSettings {
         didSet { save() }
     }
@@ -14,14 +14,14 @@ final class BaseFSettingsManager: SettingsManager, Injectable {
 
     init(resolver: Resolver) {
         let storage = resolver.resolve(FileStorage.self)!
-        settings = (try? storage.retrieve(OpenAPS.Settings.freeAPSSettings, as: FreeAPSSettings.self))
-            ?? FreeAPSSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.freeAPSSettings))
+        settings = (try? storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self))
+            ?? FreeAPSSettings(from: OpenAPS.defaults(for: OpenAPS.FreeAPS.settings))
             ?? FreeAPSSettings(units: .mmolL, closedLoop: false)
 
         injectServices(resolver)
     }
 
     private func save() {
-        try? storage.save(settings, as: OpenAPS.Settings.freeAPSSettings)
+        try? storage.save(settings, as: OpenAPS.FreeAPS.settings)
     }
 }