Ivan Valkou 5 лет назад
Родитель
Сommit
245358f346

+ 8 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -84,6 +84,7 @@
 		383948DA25CD64D500E91849 /* Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D925CD64D500E91849 /* Glucose.swift */; };
 		384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803325C385E60086DB71 /* JavaScriptWorker.swift */; };
 		384E803825C388640086DB71 /* Script.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384E803725C388640086DB71 /* Script.swift */; };
+		3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3870FF4225EC13F40088248F /* BloodGlucose.swift */; };
 		388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E595B25AD948C0019842D /* FreeAPSApp.swift */; };
 		388E596025AD948E0019842D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 388E595F25AD948E0019842D /* Assets.xcassets */; };
 		388E596C25AD95110019842D /* OpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E596B25AD95110019842D /* OpenAPS.swift */; };
@@ -135,6 +136,7 @@
 		38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */; };
 		38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C4D33925E9A1ED00D30B77 /* NSObject+AssociatedValues.swift */; };
 		38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D0B3B525EBE24900CB6E88 /* Battery.swift */; };
+		38D0B3D925EC07C400CB6E88 /* CarbHystoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38D0B3D825EC07C400CB6E88 /* CarbHystoryEntry.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */; };
 		38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */; };
@@ -593,6 +595,7 @@
 		383948D925CD64D500E91849 /* Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glucose.swift; sourceTree = "<group>"; };
 		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>"; };
+		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
 		388E595825AD948C0019842D /* FreeAPS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FreeAPS.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		388E595B25AD948C0019842D /* FreeAPSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeAPSApp.swift; sourceTree = "<group>"; };
 		388E595F25AD948E0019842D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -623,6 +626,7 @@
 		38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Extensions.swift"; sourceTree = "<group>"; };
 		38C4D33925E9A1ED00D30B77 /* NSObject+AssociatedValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+AssociatedValues.swift"; sourceTree = "<group>"; };
 		38D0B3B525EBE24900CB6E88 /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = "<group>"; };
+		38D0B3D825EC07C400CB6E88 /* CarbHystoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbHystoryEntry.swift; sourceTree = "<group>"; };
 		38FCF3D525E8FDF40078B0D1 /* MD5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MD5.swift; sourceTree = "<group>"; };
 		38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FreeAPSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		38FCF3F125E9028E0078B0D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -1069,6 +1073,8 @@
 				38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */,
 				38BF021C25E7E3AF00579895 /* Reservoir.swift */,
 				38D0B3B525EBE24900CB6E88 /* Battery.swift */,
+				38D0B3D825EC07C400CB6E88 /* CarbHystoryEntry.swift */,
+				3870FF4225EC13F40088248F /* BloodGlucose.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1664,6 +1670,7 @@
 				3811DF0525CAA62600A708ED /* DependeciesContainer.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
 				3811DF1025CAAAE200A708ED /* BaseAPSManager.swift in Sources */,
+				3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */,
 				3811DE0A25C9D32F00A708ED /* BaseModuleBuilder.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
@@ -1714,6 +1721,7 @@
 				3811DEAC25C9D88300A708ED /* NetworkManager.swift in Sources */,
 				3811DE3325C9D49500A708ED /* HomeBuilder.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
+				38D0B3D925EC07C400CB6E88 /* CarbHystoryEntry.swift in Sources */,
 				3811DE2125C9D48300A708ED /* MainBuilder.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				3811DE7925C9D6D300A708ED /* LoginViewModel.swift in Sources */,

+ 1 - 0
FreeAPS/Resources/json/defaults/monitor/carbhistory.json

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

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

@@ -6,4 +6,6 @@ protocol APSManager {
     func makeProfiles()
     var pumpManager: PumpManagerUI? { get set }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
+    func fetchLastGlucose()
+    func makeMeal()
 }

+ 19 - 0
FreeAPS/Sources/APS/BaseAPSManager.swift

@@ -5,9 +5,12 @@ import Swinject
 
 final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var storage: FileStorage!
+    @Injected() private var keychain: Keychain!
     @Injected() private var deviceDataManager: DeviceDataManager!
     private var openAPS: OpenAPS!
 
+    private var glucoseCancellable: AnyCancellable?
+
     var pumpManager: PumpManagerUI? {
         get {
             deviceDataManager.pumpManager
@@ -32,4 +35,20 @@ final class BaseAPSManager: APSManager, Injectable {
         openAPS.makeProfile(autotuned: false)
         openAPS.makeProfile(autotuned: true)
     }
+
+    func makeMeal() {
+        openAPS.makeMeal()
+    }
+
+    func fetchLastGlucose() {
+        if let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
+           let url = URL(string: urlString)
+        {
+            glucoseCancellable = NightscoutAPI(url: url).fetchLast(288)
+                .sink { _ in }
+            receiveValue: { glucose in
+                try? self.storage.append(glucose, to: OpenAPS.Monitor.glucose, uniqBy: \.date)
+            }
+        }
+    }
 }

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

@@ -44,6 +44,8 @@ extension OpenAPS {
         static let clock = "monitor/clock-zoned.json"
         static let status = "monitor/status.json"
         static let tempBasal = "monitor/temp_basal.json"
+        static let meal = "monitor/meal.json"
+        static let glucose = "monitor/glucose.json"
     }
 
     enum Function {

+ 25 - 1
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -59,6 +59,7 @@ final class OpenAPS {
             )
 
             print("MEAL: \(mealResult)")
+            try? self.storage.save(mealResult, as: Monitor.meal)
 
             let glucoseStatus = self.glucoseGetLast(glucose: glucose)
             print("GLUCOSE STATUS: \(glucoseStatus)")
@@ -103,6 +104,29 @@ final class OpenAPS {
         }
     }
 
+    func makeMeal() {
+        processQueue.async {
+            let pumphistory = self.loadFileFromStorage(name: Monitor.pumpHistory)
+            let profile = self.loadFileFromStorage(name: Settings.profile)
+            let basalProfile = self.loadFileFromStorage(name: Settings.basalProfile)
+            let clock = Date().rawJSON
+            let carbs = self.loadFileFromStorage(name: Monitor.carbHistory)
+            let glucose = self.loadFileFromStorage(name: Monitor.glucose)
+
+            let mealResult = self.meal(
+                pumphistory: pumphistory,
+                profile: profile,
+                basalProfile: basalProfile,
+                clock: clock,
+                carbs: carbs,
+                glucose: glucose
+            )
+
+            print("MEAL: \(mealResult)")
+            try? self.storage.save(mealResult, as: Monitor.meal)
+        }
+    }
+
     func makeProfile(autotuned: Bool) {
         processQueue.async {
             print("MAKE PROFILE autotuned \(autotuned)")
@@ -326,7 +350,7 @@ final class OpenAPS {
     }
 
     private func loadFileFromStorage(name: String) -> RawJSON {
-        (try? storage.retrieve(name, as: RawJSON.self)) ?? OpenAPS.defaults(for: name)
+        storage.retrieveRaw(name) ?? OpenAPS.defaults(for: name)
     }
 
     static func defaults(for file: String) -> RawJSON {

+ 34 - 7
FreeAPS/Sources/APS/PumpHistoryStorage.swift

@@ -5,6 +5,7 @@ import Swinject
 
 protocol PumpHistoryStorage {
     func storePumpEvents(_ events: [NewPumpEvent])
+    func storeBolusWizardCarbs(_ carbs: Int)
 }
 
 final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
@@ -33,7 +34,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         duration: minutes,
                         durationMin: nil,
                         rate: nil,
-                        temp: nil
+                        temp: nil,
+                        carbInput: nil
                     )]
                 case .tempBasal:
                     print("[PUMP EVENT] Temp basal event:\n\(event.title))")
@@ -49,7 +51,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             duration: nil,
                             durationMin: minutes,
                             rate: rate,
-                            temp: nil
+                            temp: nil,
+                            carbInput: nil
                         ),
                         PumpHistoryEvent(
                             id: "_" + id,
@@ -59,7 +62,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             duration: nil,
                             durationMin: nil,
                             rate: rate,
-                            temp: .absolute
+                            temp: .absolute,
+                            carbInput: nil
                         )
                     ]
                 case .suspend:
@@ -73,7 +77,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             duration: nil,
                             durationMin: nil,
                             rate: nil,
-                            temp: nil
+                            temp: nil,
+                            carbInput: nil
                         )
                     ]
                 case .resume:
@@ -87,7 +92,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             duration: nil,
                             durationMin: nil,
                             rate: nil,
-                            temp: nil
+                            temp: nil,
+                            carbInput: nil
                         )
                     ]
                 case .rewind:
@@ -101,7 +107,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             duration: nil,
                             durationMin: nil,
                             rate: nil,
-                            temp: nil
+                            temp: nil,
+                            carbInput: nil
                         )
                     ]
                 case .prime:
@@ -115,7 +122,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                             duration: nil,
                             durationMin: nil,
                             rate: nil,
-                            temp: nil
+                            temp: nil,
+                            carbInput: nil
                         )
                     ]
                 default:
@@ -127,6 +135,25 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
+    func storeBolusWizardCarbs(_ carbs: Int) {
+        processQueue.async {
+            let eventsToStore = [
+                PumpHistoryEvent(
+                    id: UUID().uuidString,
+                    type: .bolusWizard,
+                    timestamp: Date(),
+                    amount: nil,
+                    duration: nil,
+                    durationMin: nil,
+                    rate: nil,
+                    temp: nil,
+                    carbInput: carbs
+                )
+            ]
+            self.processNewEvents(eventsToStore)
+        }
+    }
+
     private func processNewEvents(_ events: [PumpHistoryEvent]) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         try? storage.transaction { storage in

+ 1 - 1
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -2,10 +2,10 @@ import SwiftUI
 import Swinject
 
 private let dependencies: [DependeciesContainer.Type] = [
+    StorageContainer.self,
     ServiceContainer.self,
     APSContainer.self,
     UIContainer.self,
-    StorageContainer.self,
     NetworkContainer.self,
     SecurityContainer.self
 ]

+ 28 - 0
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -0,0 +1,28 @@
+import Foundation
+
+struct BloodGlucose: JSON {
+    enum Direction: String, JSON {
+        case tripleUp = "TripleUp"
+        case doubleUp = "DoubleUp"
+        case singleUp = "SingleUp"
+        case fortyFiveUp = "FortyFiveUp"
+        case flat = "Flat"
+        case fortyFiveDown = "FortyFiveDown"
+        case singleDown = "SingleDown"
+        case doubleDown = "DoubleDown"
+        case tripleDown = "TripleDown"
+        case none = "NONE"
+        case notComputable = "NOT COMPUTABLE"
+        case rateOutOfRange = "RATE OUT OF RANGE"
+    }
+
+    var sgv: Int?
+    let direction: Direction?
+    let date: Date
+    let filtered: Double?
+    let noise: Int?
+
+    var glucose: Int { sgv ?? 0 }
+
+    var isStateValid: Bool { glucose >= 39 && noise ?? 1 != 4 }
+}

+ 15 - 0
FreeAPS/Sources/Models/CarbHystoryEntry.swift

@@ -0,0 +1,15 @@
+import Foundation
+
+struct CarbHystoryEntry: JSON {
+    let date: Date
+    let carbs: Int
+    let enteredBy: String?
+}
+
+extension CarbHystoryEntry {
+    private enum CodingKeys: String, CodingKey {
+        case date = "created_at"
+        case carbs
+        case enteredBy
+    }
+}

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

@@ -9,6 +9,7 @@ struct PumpHistoryEvent: JSON {
     let durationMin: Int?
     let rate: Decimal?
     let temp: PumpHistoryTempType?
+    let carbInput: Int?
 }
 
 enum PumpHistoryEventType: String, JSON {
@@ -40,5 +41,6 @@ extension PumpHistoryEvent {
         case durationMin = "duration (min)"
         case rate
         case temp
+        case carbInput = "carb_input"
     }
 }

+ 8 - 6
FreeAPS/Sources/Modules/ConfigEditor/ConfigEditorProvider.swift

@@ -5,12 +5,14 @@ extension ConfigEditor {
         @Injected() private var storage: FileStorage!
 
         func load(file: String) -> RawJSON {
-            if let value = try? storage.retrieve(file, as: RawJSON.self) {
-                return value
-            } else if let value = try? storage.retrieve(file, as: [PumpHistoryEvent].self) {
-                return value.rawJSON
-            }
-            return OpenAPS.defaults(for: file)
+//            if let value = try? storage.retrieve(file, as: RawJSON.self) {
+//                return value
+//            } else if let value = try? storage.retrieve(file, as: [PumpHistoryEvent].self) {
+//                return value.rawJSON
+//            } else if let value = try? storage.retrieve(file, as: [BloodGlucose].self) {
+//                return value.rawJSON
+//            }
+            storage.retrieveRaw(file) ?? OpenAPS.defaults(for: file)
         }
 
         func urlFor(file: String) -> URL? {

+ 8 - 0
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -11,5 +11,13 @@ extension Home {
         func makeProfiles() {
             apsManager.makeProfiles()
         }
+
+        func fetchGlucose() {
+            apsManager.fetchLastGlucose()
+        }
+
+        func makeMeal() {
+            apsManager.makeMeal()
+        }
     }
 }

+ 12 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -19,6 +19,18 @@ extension Home {
                         .foregroundColor(.white)
                         .buttonBackground()
                 }
+                Button(action: viewModel.fetchGlucose) {
+                    Text("Fetch glucose")
+                        .frame(maxWidth: .infinity)
+                        .foregroundColor(.white)
+                        .buttonBackground()
+                }
+                Button(action: viewModel.makeMeal) {
+                    Text("Make meal")
+                        .frame(maxWidth: .infinity)
+                        .foregroundColor(.white)
+                        .buttonBackground()
+                }
                 Spacer()
             }
             .padding()

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

@@ -25,6 +25,8 @@ extension Settings {
                     Text("Temp targets").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.tempTargets), from: self)
                     Text("Pump profile").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.pumpProfile), from: self)
                     Text("Profile").chevronCell().modal(for: .configEditor(file: OpenAPS.Settings.profile), from: self)
+                    Text("Glucose").chevronCell().modal(for: .configEditor(file: OpenAPS.Monitor.glucose), from: self)
+                    Text("Meal").chevronCell().modal(for: .configEditor(file: OpenAPS.Monitor.meal), from: self)
                 }
 
                 Section(header: Text("Services")) {

+ 52 - 4
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -2,14 +2,36 @@ import Combine
 import CommonCrypto
 import Foundation
 
-struct NightscoutAPI {
+class NightscoutAPI {
+    init(url: URL, secret: String? = nil) {
+        self.url = url
+        self.secret = secret
+    }
+
+    private enum Config {
+        static let entriesPath = "/api/v1/entries.json"
+        static let retryCount = 5
+    }
+
+    enum Error: LocalizedError {
+        case badStatusCode
+        case missingURL
+    }
+
     let url: URL
-    let secret: String
+    let secret: String?
+
     private let service = NetworkService()
+
+    private lazy var decoder: JSONDecoder = {
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .millisecondsSince1970
+        return decoder
+    }()
 }
 
 extension NightscoutAPI {
-    func checkConnection() -> AnyPublisher<Void, Error> {
+    func checkConnection() -> AnyPublisher<Void, Swift.Error> {
         struct Check: Codable, Equatable {
             var eventType = "Note"
             var enteredBy = "feeaps-x://"
@@ -19,12 +41,38 @@ extension NightscoutAPI {
         var request = URLRequest(url: url.appendingPathComponent("api/v1/treatments.json"))
         request.httpMethod = "POST"
         request.addValue("application/json", forHTTPHeaderField: "Content-Type")
-        request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
         request.httpBody = try! JSONEncoder().encode(check)
         return service.run(request)
             .map { _ in () }
             .eraseToAnyPublisher()
     }
+
+    func fetchLast(_ count: Int) -> AnyPublisher<[BloodGlucose], Swift.Error> {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.entriesPath
+        components.queryItems = [URLQueryItem(name: "count", value: "\(count)")]
+
+        var request = URLRequest(url: components.url!)
+        request.allowsConstrainedNetworkAccess = false
+
+        return URLSession.shared.dataTaskPublisher(for: request)
+            .retry(Config.retryCount)
+            .tryMap { output in
+                guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
+                    throw Error.badStatusCode
+                }
+                return output.data
+            }
+            .decode(type: [BloodGlucose].self, decoder: decoder)
+            .map { $0.filter { $0.isStateValid } }
+            .eraseToAnyPublisher()
+    }
 }
 
 private extension String {

+ 15 - 1
FreeAPS/Sources/Services/Storage/FileStorage.swift

@@ -4,6 +4,7 @@ import Foundation
 protocol FileStorage {
     func save<Value: JSON>(_ value: Value, as name: String) throws
     func retrieve<Value: JSON>(_ name: String, as type: Value.Type) throws -> Value
+    func retrieveRaw(_ name: String) -> RawJSON?
     func append<Value: JSON>(_ newValue: Value, to name: String) throws
     func append<Value: JSON>(_ newValues: [Value], to name: String) throws
     func append<Value: JSON, T: Equatable>(_ newValue: Value, to name: String, uniqBy keyPath: KeyPath<Value, T>) throws
@@ -33,7 +34,11 @@ final class BaseFileStorage: FileStorage {
 
     func save<Value: JSON>(_ value: Value, as name: String) throws {
         try processQueue.safeSync {
-            try Disk.save(value, to: .documents, as: name, encoder: self.encoder)
+            if let value = value as? RawJSON, let data = value.data(using: .utf8) {
+                try Disk.save(data, to: .documents, as: name)
+            } else {
+                try Disk.save(value, to: .documents, as: name, encoder: self.encoder)
+            }
         }
     }
 
@@ -43,6 +48,15 @@ final class BaseFileStorage: FileStorage {
         }
     }
 
+    func retrieveRaw(_ name: String) -> RawJSON? {
+        processQueue.safeSync {
+            guard let data = try? Disk.retrieve(name, from: .documents, as: Data.self) else {
+                return nil
+            }
+            return String(data: data, encoding: .utf8)
+        }
+    }
+
     func append<Value: JSON>(_ newValue: Value, to name: String) throws {
         try processQueue.safeSync {
             try Disk.append(newValue, to: name, in: .documents, decoder: decoder, encoder: encoder)