ソースを参照

Merge remote-tracking branch 'ivalkou/dev' into Crowdin

Jon B.M 4 年 前
コミット
15e187951a

+ 107 - 71
FreeAPS/Sources/APS/APSManager.swift

@@ -108,9 +108,7 @@ final class BaseAPSManager: APSManager, Injectable {
         lastLoopDateSubject.send(lastLoopDate)
 
         isLooping
-            .sink { value in
-                self.deviceDataManager.loopInProgress = value
-            }
+            .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
             .store(in: &lifetime)
     }
 
@@ -147,6 +145,7 @@ final class BaseAPSManager: APSManager, Injectable {
         deviceDataManager.heartbeat(date: date)
     }
 
+    // Loop entry point
     private func loop() {
         guard !isLooping.value else {
             warning(.apsManager, "Already looping, skip")
@@ -156,51 +155,71 @@ final class BaseAPSManager: APSManager, Injectable {
         debug(.apsManager, "Starting loop")
         isLooping.send(true)
         determineBasal()
-            .sink { [weak self] ok in
-                guard let self = self else { return }
+            .replaceEmpty(with: false)
+            .flatMap { [weak self] success -> AnyPublisher<Void, Error> in
+                guard let self = self, success else {
+                    return Fail(error: APSError.apsError(message: "Determine basal failed")).eraseToAnyPublisher()
+                }
 
-                if ok {
-                    self.nightscout.uploadStatus()
-                    if self.settings.closedLoop {
-                        self.enactSuggested()
-                    } else {
-                        self.isLooping.send(false)
-                        self.lastLoopDate = Date()
-                    }
+                // Open loop completed
+                guard self.settings.closedLoop else {
+                    return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
+                }
+
+                self.nightscout.uploadStatus()
+
+                // Closed loop - enact suggested
+                return self.enactSuggested()
+            }
+            .sink { [weak self] completion in
+                guard let self = self else { return }
+                if case let .failure(error) = completion {
+                    self.loopCompleted(error: error)
                 } else {
-                    self.isLooping.send(false)
+                    self.loopCompleted()
                 }
-            }.store(in: &lifetime)
+            } receiveValue: {}
+            .store(in: &lifetime)
     }
 
-    private func verifyStatus() -> Bool {
+    // Loop exit point
+    private func loopCompleted(error: Error? = nil) {
+        isLooping.send(false)
+
+        if let error = error {
+            warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
+            processError(error)
+        } else {
+            debug(.apsManager, "Loop succeeded")
+            lastLoopDate = Date()
+            lastError.send(nil)
+        }
+
+        if settings.closedLoop {
+            reportEnacted(received: error == nil)
+        }
+    }
+
+    private func verifyStatus() -> Error? {
         guard let pump = pumpManager else {
-            debug(.apsManager, "Pump is not set")
-            processError(APSError.invalidPumpState(message: "Pump not set"))
-            return false
+            return APSError.invalidPumpState(message: "Pump not set")
         }
         let status = pump.status.pumpStatus
 
         guard !status.bolusing else {
-            debug(.apsManager, "Pump is bolusing")
-            processError(APSError.invalidPumpState(message: "Pump is bolusing"))
-            return false
+            return APSError.invalidPumpState(message: "Pump is bolusing")
         }
 
         guard !status.suspended else {
-            debug(.apsManager, "Pump suspended")
-            processError(APSError.invalidPumpState(message: "Pump suspended"))
-            return false
+            return APSError.invalidPumpState(message: "Pump suspended")
         }
 
         let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
         guard reservoir > 0 else {
-            debug(.apsManager, "Reservoir is empty")
-            processError(APSError.invalidPumpState(message: "Reservoir is empty"))
-            return false
+            return APSError.invalidPumpState(message: "Reservoir is empty")
         }
 
-        return true
+        return nil
     }
 
     private func autosens() -> AnyPublisher<Bool, Never> {
@@ -301,7 +320,17 @@ final class BaseAPSManager: APSManager, Injectable {
     private var bolusReporter: DoseProgressReporter?
 
     func enactBolus(amount: Double, isSMB: Bool) {
-        guard let pump = pumpManager, verifyStatus() else { return }
+        if let error = verifyStatus() {
+            processError(error)
+            processQueue.async {
+                self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
+                    $0.bolusDidFail()
+                }
+            }
+            return
+        }
+
+        guard let pump = pumpManager else { return }
 
         let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
 
@@ -348,7 +377,12 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     func enactTempBasal(rate: Double, duration: TimeInterval) {
-        guard let pump = pumpManager, verifyStatus() else { return }
+        if let error = verifyStatus() {
+            processError(error)
+            return
+        }
+
+        guard let pump = pumpManager else { return }
         debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
 
         let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
@@ -402,7 +436,8 @@ final class BaseAPSManager: APSManager, Injectable {
 
         switch action {
         case let .bolus(amount):
-            guard verifyStatus() else {
+            if let error = verifyStatus() {
+                processError(error)
                 return
             }
             let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
@@ -419,7 +454,8 @@ final class BaseAPSManager: APSManager, Injectable {
         case let .pump(pumpAction):
             switch pumpAction {
             case .suspend:
-                guard verifyStatus(), !pump.status.pumpStatus.suspended else {
+                if let error = verifyStatus() {
+                    processError(error)
                     return
                 }
                 pump.suspendDelivery { error in
@@ -450,7 +486,11 @@ final class BaseAPSManager: APSManager, Injectable {
             debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
             announcementsStorage.storeAnnouncements([announcement], enacted: true)
         case let .tempbasal(rate, duration):
-            guard verifyStatus(), !settings.closedLoop else {
+            if let error = verifyStatus() {
+                processError(error)
+                return
+            }
+            guard !settings.closedLoop else {
                 return
             }
             let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
@@ -489,30 +529,27 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func enactSuggested() {
+    private func enactSuggested() -> AnyPublisher<Void, Error> {
         guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
-            isLooping.send(false)
-            warning(.apsManager, "Suggestion not found")
-            processError(APSError.apsError(message: "Suggestion not found"))
-            return
+            return Fail(error: APSError.apsError(message: "Suggestion not found")).eraseToAnyPublisher()
         }
 
         guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
-            isLooping.send(false)
-            warning(.apsManager, "Suggestion expired")
-            processError(APSError.apsError(message: "Suggestion expired"))
-            return
+            return Fail(error: APSError.apsError(message: "Suggestion expired")).eraseToAnyPublisher()
         }
 
         guard let pump = pumpManager else {
-            isLooping.send(false)
-            warning(.apsManager, "Pump not set")
-            processError(APSError.invalidPumpState(message: "Pump not set"))
-            return
+            return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
         }
 
         let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
-            guard let rate = suggested.rate, let duration = suggested.duration, self.verifyStatus() else {
+            if let error = self.verifyStatus() {
+                return Fail(error: error).eraseToAnyPublisher()
+            }
+
+            guard let rate = suggested.rate, let duration = suggested.duration else {
+                // It is OK, no temp required
+                debug(.apsManager, "No temp required")
                 return Just(()).setFailureType(to: Error.self)
                     .eraseToAnyPublisher()
             }
@@ -525,7 +562,12 @@ final class BaseAPSManager: APSManager, Injectable {
         }.eraseToAnyPublisher()
 
         let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
-            guard let units = suggested.units, self.verifyStatus() else {
+            if let error = self.verifyStatus() {
+                return Fail(error: error).eraseToAnyPublisher()
+            }
+            guard let units = suggested.units else {
+                // It is OK, no bolus required
+                debug(.apsManager, "No bolus required")
                 return Just(()).setFailureType(to: Error.self)
                     .eraseToAnyPublisher()
             }
@@ -536,26 +578,11 @@ final class BaseAPSManager: APSManager, Injectable {
             .eraseToAnyPublisher()
         }.eraseToAnyPublisher()
 
-        basalPublisher
-            .flatMap { bolusPublisher }
-            .sink { [weak self] completion in
-                if case let .failure(error) = completion {
-                    warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
-                    self?.reportEnacted(suggestion: suggested, received: false)
-                    self?.processError(APSError.pumpError(error))
-                } else {
-                    self?.reportEnacted(suggestion: suggested, received: true)
-                }
-                self?.isLooping.send(false)
-            } receiveValue: {
-                debug(.apsManager, "Loop succeeded")
-                self.lastError.send(nil)
-                self.lastLoopDate = Date()
-            }.store(in: &lifetime)
+        return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
     }
 
-    private func reportEnacted(suggestion: Suggestion, received: Bool) {
-        if suggestion.deliverAt != nil {
+    private func reportEnacted(received: Bool) {
+        if let suggestion = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self), suggestion.deliverAt != nil {
             var enacted = suggestion
             enacted.timestamp = Date()
             enacted.recieved = received
@@ -602,7 +629,9 @@ private extension PumpManager {
                     promise(.failure(error))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 
     func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry, Error> {
@@ -617,7 +646,9 @@ private extension PumpManager {
                     promise(.failure(error))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 
     func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
@@ -633,6 +664,7 @@ private extension PumpManager {
                 }
             }
         }
+        .mapError { APSError.pumpError($0) }
         .eraseToAnyPublisher()
     }
 
@@ -645,7 +677,9 @@ private extension PumpManager {
                     promise(.success(()))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 
     func resumeDelivery() -> AnyPublisher<Void, Error> {
@@ -657,7 +691,9 @@ private extension PumpManager {
                     promise(.success(()))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 }
 

+ 10 - 0
FreeAPS/Sources/Helpers/String+Extensions.swift

@@ -1,3 +1,5 @@
+import Foundation
+
 extension String {
     func capitalizingFirstLetter() -> String {
         prefix(1).capitalized + dropFirst()
@@ -7,3 +9,11 @@ extension String {
         self = capitalizingFirstLetter()
     }
 }
+
+extension LosslessStringConvertible {
+    var string: String { .init(self) }
+}
+
+extension FloatingPoint where Self: LosslessStringConvertible {
+    var decimal: Decimal? { Decimal(string: string) }
+}

+ 17 - 0
FreeAPS/Sources/Models/BasalProfileEntry.swift

@@ -9,3 +9,20 @@ struct BasalProfileEntry: JSON, Equatable {
 protocol BasalProfileObserver {
     func basalProfileDidChange(_ basalProfile: [BasalProfileEntry])
 }
+
+extension BasalProfileEntry {
+    private enum CodingKeys: String, CodingKey {
+        case start
+        case minutes
+        case rate
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let start = try container.decode(String.self, forKey: .start)
+        let minutes = try container.decode(Int.self, forKey: .minutes)
+        let rate = try container.decode(Double.self, forKey: .rate).decimal ?? .zero
+
+        self = BasalProfileEntry(start: start, minutes: minutes, rate: rate)
+    }
+}

+ 17 - 0
FreeAPS/Sources/Models/CarbRatios.swift

@@ -15,3 +15,20 @@ enum CarbUnit: String, JSON {
     case grams
     case exchanges
 }
+
+extension CarbRatioEntry {
+    private enum CodingKeys: String, CodingKey {
+        case start
+        case offset
+        case ratio
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let start = try container.decode(String.self, forKey: .start)
+        let offset = try container.decode(Int.self, forKey: .offset)
+        let ratio = try container.decode(Double.self, forKey: .ratio).decimal ?? .zero
+
+        self = CarbRatioEntry(start: start, offset: offset, ratio: ratio)
+    }
+}

+ 17 - 0
FreeAPS/Sources/Models/InsulinSensitivities.swift

@@ -19,3 +19,20 @@ struct InsulinSensitivityEntry: JSON {
     let offset: Int
     let start: String
 }
+
+extension InsulinSensitivityEntry {
+    private enum CodingKeys: String, CodingKey {
+        case sensitivity
+        case offset
+        case start
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let sensitivity = try container.decode(Double.self, forKey: .sensitivity).decimal ?? .zero
+        let start = try container.decode(String.self, forKey: .start)
+        let offset = try container.decode(Int.self, forKey: .offset)
+
+        self = InsulinSensitivityEntry(sensitivity: sensitivity, offset: offset, start: start)
+    }
+}

+ 2 - 1
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -15,7 +15,8 @@ extension BasalProfileEditor {
         }
 
         override func subscribe() {
-            rateValues = provider.supportedBasalRates ?? stride(from: Decimal(0.05), to: 10.01, by: 0.05).map { $0 }
+            rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
+                .map { ($0.decimal ?? .zero) / 100 }
             items = provider.profile.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.minutes * 60)) ?? 0
                 let rateIndex = rateValues.firstIndex(of: value.rate) ?? 0

+ 1 - 1
FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift

@@ -7,7 +7,7 @@ extension CREditor {
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
-        let rateValues = stride(from: 3, to: 50.01, by: 0.1).map { Decimal($0) }
+        let rateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
 
         var canAdd: Bool {
             guard let lastItem = items.last else { return true }

+ 1 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -14,7 +14,7 @@ extension ISFEditor {
             case .mgdL:
                 return stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
             case .mmolL:
-                return stride(from: 0.1, to: 30.01, by: 0.1).map { Decimal($0) }
+                return stride(from: 1.0, to: 301.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
             }
         }