Просмотр исходного кода

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

Jon B.M 4 лет назад
Родитель
Сommit
7d45ce6b58
34 измененных файлов с 611 добавлено и 444 удалено
  1. 2 2
      Dependencies/LoopKit/LoopKit/tr.lproj/Localizable.strings
  2. 1 1
      Dependencies/LoopKit/LoopKitUI/tr.lproj/Localizable.strings
  3. 23 23
      Dependencies/rileylink_ios/OmniKitUI/tr.lproj/OmnipodPumpManager.strings
  4. 7 7
      FreeAPS/Resources/fr.lproj/InfoPlist.strings
  5. 107 71
      FreeAPS/Sources/APS/APSManager.swift
  6. 10 0
      FreeAPS/Sources/Helpers/String+Extensions.swift
  7. 3 0
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  8. 3 0
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  9. 3 0
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  10. 3 0
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  11. 3 0
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  12. 3 0
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  13. 189 168
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  14. 3 0
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  15. 3 0
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  16. 8 5
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  17. 145 142
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  18. 3 0
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  19. 3 0
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  20. 1 0
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  21. 3 0
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  22. 3 0
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  23. 16 13
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  24. 3 0
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  25. 3 0
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  26. 17 0
      FreeAPS/Sources/Models/BasalProfileEntry.swift
  27. 17 0
      FreeAPS/Sources/Models/CarbRatios.swift
  28. 1 1
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  29. 17 0
      FreeAPS/Sources/Models/InsulinSensitivities.swift
  30. 2 1
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  31. 1 1
      FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift
  32. 1 1
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  33. 2 4
      README.md
  34. 2 4
      README_RU.md

+ 2 - 2
Dependencies/LoopKit/LoopKit/tr.lproj/Localizable.strings

@@ -14,7 +14,7 @@
 "com.loudnate.CarbKit.deleteCarbEntryUnownedErrorDescription" = "Yetkilendirme Reddedildi";
 
 /* The error recovery suggestion when attempting to delete a sample not shared by the current app */
-"com.loudnate.carbKit.sharingDeniedErrorRecoverySuggestion" = "Bu örnek Sağlık uygulamasından silinebilir";
+"com.loudnate.carbKit.sharingDeniedErrorRecoverySuggestion" = "Bu örnek Health uygulamasından silinebilir";
 
 /* Generic pump error description */
 "Communication Failure" = "İletişim Hatası";
@@ -68,7 +68,7 @@
 "OK" = "Tamam";
 
 /* The error recovery suggestion when Health sharing was denied */
-"Please re-enable sharing in Health" = "Lütfen Sağlık Uygulaması paylaşımını yeniden etkinleştirin";
+"Please re-enable sharing in Health" = "Lütfen Health paylaşımı yeniden etkinleştirin";
 
 /* Glucose trend up */
 "Rising" = "Yükselmekte";

+ 1 - 1
Dependencies/LoopKit/LoopKitUI/tr.lproj/Localizable.strings

@@ -45,7 +45,7 @@
 "Basal, bolus, and correction insulin dose amounts are unaffected." = "Bazal, bolus ve düzeltme insülin doz miktarları etkilenmez.";
 
 /* The title of the cancel action in an action sheet */
-"Cancel" = "İptal Et";
+"Cancel" = "Vazgeç";
 
 /* The text for the override cancellation button */
 "Cancel Override" = "Geçersiz kılmayı İptal et";

+ 23 - 23
Dependencies/rileylink_ios/OmniKitUI/tr.lproj/OmnipodPumpManager.strings

@@ -1,68 +1,68 @@
 /* Class = "UITableViewSection"; headerTitle = "Remove POD"; ObjectID = "1LF-te-Bdd"; */
-"1LF-te-Bdd.headerTitle" = "Remove POD";
+"1LF-te-Bdd.headerTitle" = "POD'u kaldır";
 
 /* Class = "UINavigationItem"; title = "RileyLink Setup"; ObjectID = "3HH-eJ-lRh"; */
-"3HH-eJ-lRh.title" = "RileyLink Setup";
+"3HH-eJ-lRh.title" = "RileyLink Kurulumu";
 
 /* Class = "UITableViewController"; title = "Pod Settings"; ObjectID = "6vo-Ov-UpE"; */
-"6vo-Ov-UpE.title" = "Pod Settings";
+"6vo-Ov-UpE.title" = "Pod Ayarları";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "91O-Un-vKc"; */
-"91O-Un-vKc.title" = "Pump Setup";
+"91O-Un-vKc.title" = "Pompa Kurulumu";
 
 /* Class = "UITableViewSection"; footerTitle = "NOTE: Do not remove the pod's needle cap at this time."; ObjectID = "EUt-xk-Rmp"; */
-"EUt-xk-Rmp.footerTitle" = "NOTE: Do not remove the pod's needle cap at this time.";
+"EUt-xk-Rmp.footerTitle" = "NOT: Şu anda pod'un plastik iğne kapağını çıkarmayın.";
 
 /* Class = "UITableViewSection"; headerTitle = "Prepare Pod"; ObjectID = "EUt-xk-Rmp"; */
-"EUt-xk-Rmp.headerTitle" = "Prepare Pod";
+"EUt-xk-Rmp.headerTitle" = "Pod'u Hazırla";
 
 /* Class = "UILabel"; text = "Loop will remind you to change your pod before it expires. You can change this to a time convenient for you."; ObjectID = "Eng-IY-fQ7"; */
-"Eng-IY-fQ7.text" = "Loop will remind you to change your pod before it expires. You can change this to a time convenient for you.";
+"Eng-IY-fQ7.text" = "Döngü, süresi dolmadan önce pod'u değiştirmenizi hatırlatacaktır. Bunun için değişimi size uygun olan bir zamanda yapabilirsiniz.";
 
 /* Class = "UILabel"; text = "Please deactivate the pod. When deactivation is complete, remove pod from body."; ObjectID = "GK7-jb-tyY"; */
-"GK7-jb-tyY.text" = "Please deactivate the pod. When deactivation is complete, remove pod from body.";
+"GK7-jb-tyY.text" = "Lütfen pod'u devre dışı bırakın. Devre dışı bırakma tamamlandığında, pod'u vücudunuzdan çıkarın.";
 
 /* Class = "UINavigationItem"; title = "Insert Cannula"; ObjectID = "HwT-30-f0y"; */
-"HwT-30-f0y.title" = "Insert Cannula";
+"HwT-30-f0y.title" = "Kanül Yerleştir";
 
 /* Class = "UILabel"; text = "Prepare site. Remove the pod's needle cap and adhesive backing. If pod is OK, apply to site."; ObjectID = "Iuv-5M-bDH"; */
-"Iuv-5M-bDH.text" = "Prepare site. Remove the pod's needle cap and adhesive backing. If pod is OK, apply to site.";
+"Iuv-5M-bDH.text" = "İnfüzyon bölgesini hazırlayın. Pod'un plastik iğne kapağını ve yapışkan desteğini çıkarın. Herşey yolunda ise pod'u infüzyon bölgesine yapıştırın.";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "aNg-mm-Uuy"; */
-"aNg-mm-Uuy.title" = "Pump Setup";
+"aNg-mm-Uuy.title" = "Pompa Kurulumu";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "ack-ra-XH6"; */
-"ack-ra-XH6.title" = "Pump Setup";
+"ack-ra-XH6.title" = "Pompa Kurulumu";
 
 /* Class = "UILabel"; text = "Your Pod is ready for use."; ObjectID = "bJ5-iH-fnF"; */
-"bJ5-iH-fnF.text" = "Your Pod is ready for use.";
+"bJ5-iH-fnF.text" = "Pod'unuz kullanıma hazır.";
 
 /* Class = "UILabel"; text = "Reminder"; ObjectID = "ePA-6p-q8C"; */
-"ePA-6p-q8C.text" = "Reminder";
+"ePA-6p-q8C.text" = "Hatırlatıcı";
 
 /* Class = "UINavigationItem"; title = "Pod Pairing"; ObjectID = "jVO-Ut-MhL"; */
-"jVO-Ut-MhL.title" = "Pod Pairing";
+"jVO-Ut-MhL.title" = "Pod Eşleştiriliyor";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "k1Y-x4-m0a"; */
-"k1Y-x4-m0a.title" = "Pump Setup";
+"k1Y-x4-m0a.title" = "Pompa Kurulumu";
 
 /* Class = "UILabel"; text = "Review your settings below. They will be programmed into the pod during pairing. You can change these settings at any time in Loopʼs Settings screen."; ObjectID = "kLL-SQ-K0a"; */
-"kLL-SQ-K0a.text" = "Review your settings below. They will be programmed into the pod during pairing. You can change these settings at any time in Loopʼs Settings screen.";
+"kLL-SQ-K0a.text" = "Aşağıdaki ayarlarınızı gözden geçirin. Pod eşleştirme sırasında bu ayarlar programlanacaktır. Bu ayarları istediğiniz zaman Loop'un Ayarlar ekranından değiştirebilirsiniz.";
 
 /* Class = "UINavigationItem"; title = "Setup Complete"; ObjectID = "nDb-R5-e02"; */
-"nDb-R5-e02.title" = "Setup Complete";
+"nDb-R5-e02.title" = "Kurulum tamamlandı";
 
 /* Class = "UITableViewSection"; footerTitle = "NOTE: If cannula sticks out, press cancel."; ObjectID = "rcC-ke-lUP"; */
-"rcC-ke-lUP.footerTitle" = "NOTE: If cannula sticks out, press cancel.";
+"rcC-ke-lUP.footerTitle" = "NOT: Kanül dışarı çıkarsa, iptal düğmesine basın.";
 
 /* Class = "UITableViewSection"; headerTitle = "Apply POD"; ObjectID = "rcC-ke-lUP"; */
-"rcC-ke-lUP.headerTitle" = "Apply POD";
+"rcC-ke-lUP.headerTitle" = "POD'u yerleştirin";
 
 /* Class = "UILabel"; text = "Label"; ObjectID = "vEc-Km-ewe"; */
-"vEc-Km-ewe.text" = "Label";
+"vEc-Km-ewe.text" = "Etiket";
 
 /* Class = "UILabel"; text = "Fill a new pod with insulin. Listen for 2 beeps from the pod during filling. Keep RileyLink adjacent to the pod during pairing."; ObjectID = "vmF-Dc-3DS"; */
-"vmF-Dc-3DS.text" = "Fill a new pod with insulin. Listen for 2 beeps from the pod during filling. Keep the RileyLink about 6 inches from the pod during pairing.";
+"vmF-Dc-3DS.text" = "Yeni bir pod'u insülinle doldurun. Doldurma sırasında pod'tan gelen 2 bip sesini dinleyin. Eşleştirme sırasında RileyLink'i bölmeden yaklaşık 6 inç ( ≈ 15cm) uzakta tutun.";
 
 /* Class = "UINavigationItem"; title = "Replace Pod"; ObjectID = "yy1-xf-HdR"; */
-"yy1-xf-HdR.title" = "Replace Pod";
+"yy1-xf-HdR.title" = "Pod'u değiştir";

+ 7 - 7
FreeAPS/Resources/fr.lproj/InfoPlist.strings

@@ -1,20 +1,20 @@
 /* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
+"NFCReaderUsageDescription" = "NFC est utilisé pour scanner les capteurs Libre.";
 
 /* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
+"NSBluetoothAlwaysUsageDescription" = "Bluetooth est utilisé pour communiquer avec la pompe à insuline et les dispositifs de surveillance continue du glucose";
 
 /* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
+"NSBluetoothPeripheralUsageDescription" = "Bluetooth est utilisé pour communiquer avec la pompe à insuline et les dispositifs de surveillance continue du glucose";
 
 /* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
+"NSFaceIDUsageDescription" = "Pour les accès autorisés au bolus";
 
 /* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
+"NSCalendarsUsageDescription" = "Le calendrier est utilisé pour créer un nouvel événement de glycémie.";
 
 /* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthUpdateUsageDescription" = "L'application Santé est utilisée pour stocker les données de glycémie";
 
 /* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthShareUsageDescription" = "L'application Santé est utilisée pour stocker les données de glycémie";

+ 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) }
+}

+ 3 - 0
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS Haupteinstellungen";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

Разница между файлами не показана из-за своего большого размера
+ 189 - 168
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


+ 3 - 0
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings

@@ -897,6 +897,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "Impostazioni principali di OpenAPS";

+ 8 - 5
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings

@@ -879,19 +879,22 @@ Enact a temp Basal or a temp target */
 "Bolus failed or inaccurate. Check pump history before repeating." = "Bolus mislyktes eller var upresis. Kontroller bolus-historikken før du gjentar.";
 
 /* */
-"Carbs" = "Carbs";
+"Carbs" = "Karbo";
 
 /* */
-"Temp Basal" = "Temp Basal";
+"Temp Basal" = "Midlertidig basal";
 
 /* */
-"Temp Target" = "Temp Target";
+"Temp Target" = "Midlertidig mål";
 
 /* */
-"Resume" = "Resume";
+"Resume" = "Gjenoppta leveranse";
 
 /* */
-"Suspend" = "Suspend";
+"Suspend" = "Pause leveranse";
+
+/* */
+"Animated Background" = "Animert bakgrunn";
 
 
 /* Headers for settings ----------------------- */

Разница между файлами не показана из-за своего большого размера
+ 145 - 142
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


+ 3 - 0
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings

@@ -895,6 +895,9 @@ Połączono z Nightscout!";
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 1 - 0
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings

@@ -896,6 +896,7 @@ Enact a temp Basal or a temp target */
 /* */
 "Animated Background" = "Анимированный фон";
 
+
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "Основные настройки OpenAPS";
 

+ 3 - 0
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Pausa";
 
+/* */
+"Animated Background" = "Animerad Bakgrund";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS huvudsakliga inställningar";

+ 16 - 13
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings

@@ -522,7 +522,7 @@ Enact a temp Basal or a temp target */
 "Pair Sensor & connect" = "Sensör eşleştir ve bağlan";
 
 /* */
-"Phone NFC required!" = "Telefon NFC gerekli!";
+"Phone NFC required!" = "NFC özellikli telefon gerekli!";
 
 /* */
 "Your phone or app is not enabled for NFC communications, which is needed to pair to libre2 sensors" = "Telefonunuz veya uygulamanız, libre2 sensörleriyle eşleştirmek için gerekli olan NFC iletişimleri için etkin değil";
@@ -540,7 +540,7 @@ Enact a temp Basal or a temp target */
 "Delete CGMManager and start anew. Your libreoopweb credentials will be preserved" = "CGMManager'ı silin ve yeniden başlayın. libreoopweb kimlik bilgileriniz korunacaktır";
 
 /* Invalid libre checksum */
-"Invalid libre checksum" = "Geçersiz libre sağlama toplamı";
+"Invalid libre checksum" = "Libre sağlaması geçersiz";
 
 /* Libre sensor was incorrectly read, CRCs were not valid */
 "Libre sensor was incorrectly read, CRCs were not valid" = "Libre sensörü hatalı okundu, CRC'ler geçersizdi";
@@ -570,10 +570,10 @@ Enact a temp Basal or a temp target */
 "This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor" = "Bu aralıklı bir sorun olabilir ancak lütfen vericinizin sensörünüze sıkıca sabitlendiğini kontrol edin";
 
 /* New Sensor Detected */
-"New Sensor Detected" = "Yemi Sensör Tespit Edildi";
+"New Sensor Detected" = "Yeni Sensör Algılandı";
 
 /* Please wait up to 30 minutes before glucose readings are available! */
-"Please wait up to 30 minutes before glucose readings are available!" = "Lütfen glikoz ölçümleri mevcut olana kadar 30 dakikaya kadar bekleyin!";
+"Please wait up to 30 minutes before glucose readings are available!" = "Lütfen glikoz ölçümleri mevcut olana kadar 30 dakika bekleyin!";
 
 /* Invalid Glucose sample detected, try again later */
 "Invalid Glucose sample detected, try again later" = "Geçersiz Glikoz örneği tespit edildi, daha sonra tekrar deneyin";
@@ -624,7 +624,7 @@ Enact a temp Basal or a temp target */
 "Last measurement" = "Son ölçüm";
 
 /* */
-"Sensor Footer checksum" = "Sensör Altbilgi sağlama toplamı";
+"Sensor Footer checksum" = "Sensör Altbilgi sağlaması";
 
 /* */
 "Last Blood Sugar prediction" = "Son KŞ'i tahmini";
@@ -642,7 +642,7 @@ Enact a temp Basal or a temp target */
 "Sensor Age Left" = "Kalan Sensör Yaşı";
 
 /* */
-"Sensor Endtime" = "Sensör Bitim zamanı";
+"Sensor Endtime" = "Sensör Bitiş zamanı";
 
 /* */
 "Sensor State" = "Sensör Durumu";
@@ -780,10 +780,10 @@ Enact a temp Basal or a temp target */
 "High Glucose Alarm active" = "Yüksek Glikoz Uyarılarımı aktif";
 
 /* */
-"Low Glucose Alarm active" = "Düşük Glikoz Uyarılarımı aktif";
+"Low Glucose Alarm active" = "Düşük Glikoz Alarmı aktif";
 
 /* */
-"No Glucose Alarm active" = "Glikoz Uyarılarımı aktif değil";
+"No Glucose Alarm active" = "Glikoz Alarmı aktif değil";
 
 /* */
 "snoozing until %@" = "%@'e kadar erteleniyor";
@@ -879,19 +879,22 @@ Enact a temp Basal or a temp target */
 "Bolus failed or inaccurate. Check pump history before repeating." = "Bolus başarısız veya hatalı. Tekrarlamadan önce pompa geçmişini kontrol edin.";
 
 /* */
-"Carbs" = "Carbs";
+"Carbs" = "Karbonhidrat";
 
 /* */
-"Temp Basal" = "Temp Basal";
+"Temp Basal" = "Geçici Bazal Oranı";
 
 /* */
-"Temp Target" = "Temp Target";
+"Temp Target" = "Geçici Hedef";
 
 /* */
-"Resume" = "Resume";
+"Resume" = "Devam et";
 
 /* */
-"Suspend" = "Suspend";
+"Suspend" = "Askıya al";
+
+/* */
+"Animated Background" = "Animasyonlu Arka Plan";
 
 
 /* Headers for settings ----------------------- */

+ 3 - 0
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS 主要设置";

+ 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)
+    }
+}

+ 1 - 1
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -22,7 +22,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var lowGlucose: Decimal = 72
     var highGlucose: Decimal = 270
     var carbsRequiredThreshold: Decimal = 10
-    var animatedBackground: Bool = true
+    var animatedBackground: Bool = false
 }
 
 extension FreeAPSSettings: Decodable {

+ 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 }
             }
         }
 

+ 2 - 4
README.md

@@ -12,7 +12,7 @@ FreeAPS X uses original JavaScript files of oref0 and provides a user interface
 
 ## Smartphone requirements
 
-- All iPhones which support iOS 14 and up.
+- All iPhones which support iOS 15 and up.
 
 ## Supported pumps
 
@@ -39,13 +39,11 @@ A stable version means that it has been tested for a long time and does not cont
 
 Stable version numbers end in **.0**.
 
-The current stable version is 0.2.0. (**DOES NOT work on iOS 15! Use beta versions**)
-
 ### Beta versions
 
 Beta versions are the first to introduce new functionality. They are designed to test and identify issues and bugs.
 
-**Beta versions are less stable, use with caution!**
+**Beta versions are fairly stable, but may contain occasional bugs.**
 
 Beta numbers end with a number greater than **0**.
 

+ 2 - 4
README_RU.md

@@ -12,7 +12,7 @@ FreeAPS X использует оригинальные JavaScript файлы or
 
 ## Требования к смартфону
 
-- Все iPhone с поддержкой iOS 14 и выше.
+- Все iPhone с поддержкой iOS 15 и выше.
 
 ## Поддерживаемые помпы
 
@@ -39,13 +39,11 @@ FreeAPS X находится в состоянии активной разраб
 
 Номера стабильных версий заканчиваются на **.0**.
 
-Текущая стабильная версия - 0.2.0 (**НЕ работает на iOS 15! Используйте бета-версии**).
-
 ### Бета-версии
 
 В бета-версиях впервые появляется новая функциональность. Они предназначены для тестирования и выявления проблем и багов.
 
-**Бета-версии менее стабильны, используйте с осторожностью!**
+**Бета-версии довольно стабильны, но могут содержать случайные ошибки.**
 
 Номера бета-версий заканчиваются на число больше **0**.