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

+ 20 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -132,6 +132,9 @@
 		38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF021A25E7D06400579895 /* PumpSettingsView.swift */; };
 		38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF021A25E7D06400579895 /* PumpSettingsView.swift */; };
 		38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF021C25E7E3AF00579895 /* Reservoir.swift */; };
 		38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF021C25E7E3AF00579895 /* Reservoir.swift */; };
 		38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF021E25E7F0DE00579895 /* DeviceDataManager.swift */; };
 		38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF021E25E7F0DE00579895 /* DeviceDataManager.swift */; };
+		38BF022725E855D300579895 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF022425E855D200579895 /* AnyDecodable.swift */; };
+		38BF022825E855D300579895 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF022525E855D300579895 /* AnyCodable.swift */; };
+		38BF022925E855D300579895 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38BF022625E855D300579895 /* AnyEncodable.swift */; };
 		38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826925CC82DB001FF17A /* NetworkService.swift */; };
 		38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826925CC82DB001FF17A /* NetworkService.swift */; };
 		38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */; };
 		38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826C25CC8461001FF17A /* NightscoutAPI.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
@@ -607,6 +610,9 @@
 		38BF021A25E7D06400579895 /* PumpSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpSettingsView.swift; sourceTree = "<group>"; };
 		38BF021A25E7D06400579895 /* PumpSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpSettingsView.swift; sourceTree = "<group>"; };
 		38BF021C25E7E3AF00579895 /* Reservoir.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reservoir.swift; sourceTree = "<group>"; };
 		38BF021C25E7E3AF00579895 /* Reservoir.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reservoir.swift; sourceTree = "<group>"; };
 		38BF021E25E7F0DE00579895 /* DeviceDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDataManager.swift; sourceTree = "<group>"; };
 		38BF021E25E7F0DE00579895 /* DeviceDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDataManager.swift; sourceTree = "<group>"; };
+		38BF022425E855D200579895 /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; };
+		38BF022525E855D300579895 /* AnyCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = "<group>"; };
+		38BF022625E855D300579895 /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
 		38FE826925CC82DB001FF17A /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
 		38FE826925CC82DB001FF17A /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
 		38FE826C25CC8461001FF17A /* NightscoutAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPI.swift; sourceTree = "<group>"; };
 		38FE826C25CC8461001FF17A /* NightscoutAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutAPI.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
@@ -1044,6 +1050,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				38BF022325E855C000579895 /* AnyCodable */,
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				3811DE5425C9D4D500A708ED /* Formatters.swift */,
 				3811DE5425C9D4D500A708ED /* Formatters.swift */,
 				3811DE5725C9D4D500A708ED /* ProgressBar.swift */,
 				3811DE5725C9D4D500A708ED /* ProgressBar.swift */,
@@ -1160,6 +1167,16 @@
 			path = SwiftNotificationCenter;
 			path = SwiftNotificationCenter;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		38BF022325E855C000579895 /* AnyCodable */ = {
+			isa = PBXGroup;
+			children = (
+				38BF022525E855D300579895 /* AnyCodable.swift */,
+				38BF022425E855D200579895 /* AnyDecodable.swift */,
+				38BF022625E855D300579895 /* AnyEncodable.swift */,
+			);
+			path = AnyCodable;
+			sourceTree = "<group>";
+		};
 		4E8C7B59F8065047ECE20965 /* View */ = {
 		4E8C7B59F8065047ECE20965 /* View */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -1620,6 +1637,7 @@
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
+				38BF022725E855D300579895 /* AnyDecodable.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
 				3811DE7A25C9D6D300A708ED /* LoginDataFlow.swift in Sources */,
 				3811DE7A25C9D6D300A708ED /* LoginDataFlow.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
@@ -1631,6 +1649,7 @@
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DEC525C9D99900A708ED /* StorageContainer.swift in Sources */,
 				3811DEC525C9D99900A708ED /* StorageContainer.swift in Sources */,
 				3811DE7F25C9D6D300A708ED /* LoginBuilder.swift in Sources */,
 				3811DE7F25C9D6D300A708ED /* LoginBuilder.swift in Sources */,
+				38BF022925E855D300579895 /* AnyEncodable.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
@@ -1657,6 +1676,7 @@
 				3811DF0825CAAA4700A708ED /* ServiceContainer.swift in Sources */,
 				3811DF0825CAAA4700A708ED /* ServiceContainer.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DE4D25C9D4B800A708ED /* AuthotizedRootViewModel.swift in Sources */,
 				3811DE4D25C9D4B800A708ED /* AuthotizedRootViewModel.swift in Sources */,
+				38BF022825E855D300579895 /* AnyCodable.swift in Sources */,
 				3811DE6A25C9D62600A708ED /* OnboardingBuilder.swift in Sources */,
 				3811DE6A25C9D62600A708ED /* OnboardingBuilder.swift in Sources */,
 				3811DEC425C9D99900A708ED /* NetworkContainer.swift in Sources */,
 				3811DEC425C9D99900A708ED /* NetworkContainer.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,

+ 27 - 6
FreeAPS.xcodeproj/xcuserdata/i.valkou.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -4,6 +4,27 @@
 <dict>
 <dict>
 	<key>SchemeUserState</key>
 	<key>SchemeUserState</key>
 	<dict>
 	<dict>
+		<key>AnyCodable (Playground) 1.xcscheme</key>
+		<dict>
+			<key>isShown</key>
+			<false/>
+			<key>orderHint</key>
+			<integer>22</integer>
+		</dict>
+		<key>AnyCodable (Playground) 2.xcscheme</key>
+		<dict>
+			<key>isShown</key>
+			<false/>
+			<key>orderHint</key>
+			<integer>23</integer>
+		</dict>
+		<key>AnyCodable (Playground).xcscheme</key>
+		<dict>
+			<key>isShown</key>
+			<false/>
+			<key>orderHint</key>
+			<integer>18</integer>
+		</dict>
 		<key>FreeAPS X.xcscheme_^#shared#^_</key>
 		<key>FreeAPS X.xcscheme_^#shared#^_</key>
 		<dict>
 		<dict>
 			<key>orderHint</key>
 			<key>orderHint</key>
@@ -82,42 +103,42 @@
 			<key>isShown</key>
 			<key>isShown</key>
 			<false/>
 			<false/>
 			<key>orderHint</key>
 			<key>orderHint</key>
-			<integer>6</integer>
+			<integer>16</integer>
 		</dict>
 		</dict>
 		<key>Sample-iOS (Playground) 2.xcscheme</key>
 		<key>Sample-iOS (Playground) 2.xcscheme</key>
 		<dict>
 		<dict>
 			<key>isShown</key>
 			<key>isShown</key>
 			<false/>
 			<false/>
 			<key>orderHint</key>
 			<key>orderHint</key>
-			<integer>7</integer>
+			<integer>17</integer>
 		</dict>
 		</dict>
 		<key>Sample-iOS (Playground).xcscheme</key>
 		<key>Sample-iOS (Playground).xcscheme</key>
 		<dict>
 		<dict>
 			<key>isShown</key>
 			<key>isShown</key>
 			<false/>
 			<false/>
 			<key>orderHint</key>
 			<key>orderHint</key>
-			<integer>5</integer>
+			<integer>15</integer>
 		</dict>
 		</dict>
 		<key>SwiftDate (Playground) 1.xcscheme</key>
 		<key>SwiftDate (Playground) 1.xcscheme</key>
 		<dict>
 		<dict>
 			<key>isShown</key>
 			<key>isShown</key>
 			<false/>
 			<false/>
 			<key>orderHint</key>
 			<key>orderHint</key>
-			<integer>19</integer>
+			<integer>20</integer>
 		</dict>
 		</dict>
 		<key>SwiftDate (Playground) 2.xcscheme</key>
 		<key>SwiftDate (Playground) 2.xcscheme</key>
 		<dict>
 		<dict>
 			<key>isShown</key>
 			<key>isShown</key>
 			<false/>
 			<false/>
 			<key>orderHint</key>
 			<key>orderHint</key>
-			<integer>20</integer>
+			<integer>21</integer>
 		</dict>
 		</dict>
 		<key>SwiftDate (Playground).xcscheme</key>
 		<key>SwiftDate (Playground).xcscheme</key>
 		<dict>
 		<dict>
 			<key>isShown</key>
 			<key>isShown</key>
 			<false/>
 			<false/>
 			<key>orderHint</key>
 			<key>orderHint</key>
-			<integer>18</integer>
+			<integer>19</integer>
 		</dict>
 		</dict>
 	</dict>
 	</dict>
 	<key>SuppressBuildableAutocreation</key>
 	<key>SuppressBuildableAutocreation</key>

+ 4 - 2
FreeAPS/Sources/APS/BaseAPSManager.swift

@@ -4,8 +4,9 @@ import LoopKitUI
 import Swinject
 import Swinject
 
 
 final class BaseAPSManager: APSManager, Injectable {
 final class BaseAPSManager: APSManager, Injectable {
+    @Injected() var storage: FileStorage!
     private var openAPS: OpenAPS!
     private var openAPS: OpenAPS!
-    private let deviceDataManager = DeviceDataManager()
+    private var deviceDataManager: DeviceDataManager!
 
 
     var pumpManager: PumpManagerUI? {
     var pumpManager: PumpManagerUI? {
         get {
         get {
@@ -20,7 +21,8 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
-        openAPS = OpenAPS(storage: resolver.resolve(FileStorage.self)!)
+        deviceDataManager = DeviceDataManager(storage: storage)
+        openAPS = OpenAPS(storage: storage)
     }
     }
 
 
     func runTest() {
     func runTest() {

+ 39 - 2
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -17,6 +17,8 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = stati
 }
 }
 
 
 final class DeviceDataManager {
 final class DeviceDataManager {
+    private let storage: FileStorage
+
     var pumpManager: PumpManagerUI? {
     var pumpManager: PumpManagerUI? {
         didSet {
         didSet {
             pumpManager?.pumpManagerDelegate = self
             pumpManager?.pumpManagerDelegate = self
@@ -31,7 +33,8 @@ final class DeviceDataManager {
 
 
     let pumpDisplayState = CurrentValueSubject<PumpDisplayState?, Never>(nil)
     let pumpDisplayState = CurrentValueSubject<PumpDisplayState?, Never>(nil)
 
 
-    init() {
+    init(storage: FileStorage) {
+        self.storage = storage
         setupPumpManager()
         setupPumpManager()
     }
     }
 
 
@@ -58,6 +61,40 @@ final class DeviceDataManager {
 
 
         return staticPumpManagersByIdentifier[managerIdentifier]
         return staticPumpManagersByIdentifier[managerIdentifier]
     }
     }
+
+    private func storePumpEvents(_ events: [NewPumpEvent]) {
+        print(
+            "[DeviceDataManager] new pump events: \(events.compactMap(\.type))"
+        )
+
+        let numberFormatter = NumberFormatter()
+        numberFormatter.numberStyle = .decimal
+
+        let eventsToStore = events.flatMap { event -> [PumpHystoryEvent] in
+            switch event.type {
+            case .bolus:
+                guard let dose = event.dose else { return [] }
+                let decimal = Decimal(string: dose.unitsInDeliverableIncrements.description)
+                return [PumpHystoryEvent(
+                    type: .bolus,
+                    timestamp: event.date,
+                    amount: decimal,
+                    duration: nil,
+                    durationMin: nil,
+                    rate: nil,
+                    temp: nil
+                )]
+            default:
+                return []
+            }
+        }
+
+        do {
+            try storage.append(eventsToStore, to: OpenAPS.Monitor.pumpHistory)
+        } catch {
+            try? storage.save(eventsToStore, as: OpenAPS.Monitor.pumpHistory)
+        }
+    }
 }
 }
 
 
 extension DeviceDataManager: PumpManagerDelegate {
 extension DeviceDataManager: PumpManagerDelegate {
@@ -101,7 +138,7 @@ extension DeviceDataManager: PumpManagerDelegate {
         lastReconciliation _: Date?,
         lastReconciliation _: Date?,
         completion: @escaping (_ error: Error?) -> Void
         completion: @escaping (_ error: Error?) -> Void
     ) {
     ) {
-        print("[DeviceDataManager] new pump events: \(events.compactMap(\.dose?.type))")
+        storePumpEvents(events)
         completion(nil)
         completion(nil)
     }
     }
 
 

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

@@ -27,6 +27,10 @@ extension OpenAPS {
         static let autosense = "settings/autosense.json"
         static let autosense = "settings/autosense.json"
     }
     }
 
 
+    enum Monitor {
+        static let pumpHistory = "monitor/pumphistory.json"
+    }
+
     enum Function {
     enum Function {
         static let freeaps = "freeaps"
         static let freeaps = "freeaps"
         static let generate = "generate"
         static let generate = "generate"

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

@@ -36,7 +36,7 @@ final class JavaScriptWorker {
     }
     }
 
 
     func call(function: String, with arguments: [JSON]) -> RawJSON {
     func call(function: String, with arguments: [JSON]) -> RawJSON {
-        let joined = arguments.map(\.string).joined(separator: ",")
+        let joined = arguments.map(\.rawJSON).joined(separator: ",")
         return json(for: "\(function)(\(joined))")
         return json(for: "\(function)(\(joined))")
     }
     }
 
 

+ 112 - 0
FreeAPS/Sources/Helpers/AnyCodable/AnyCodable.swift

@@ -0,0 +1,112 @@
+/**
+ A type-erased `Codable` value.
+
+ The `AnyCodable` type forwards encoding and decoding responsibilities
+ to an underlying value, hiding its specific underlying type.
+
+ You can encode or decode mixed-type values in dictionaries
+ and other collections that require `Encodable` or `Decodable` conformance
+ by declaring their contained type to be `AnyCodable`.
+
+ - SeeAlso: `AnyEncodable`
+ - SeeAlso: `AnyDecodable`
+ */
+
+import Foundation
+
+#if swift(>=5.1)
+    @frozen public struct AnyCodable: Codable {
+        public let value: Any
+
+        public init<T>(_ value: T?) {
+            self.value = value ?? ()
+        }
+    }
+#else
+    public struct AnyCodable: Codable {
+        public let value: Any
+
+        public init<T>(_ value: T?) {
+            self.value = value ?? ()
+        }
+    }
+#endif
+
+extension AnyCodable: _AnyEncodable, _AnyDecodable {}
+
+extension AnyCodable: Equatable {
+    public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
+        switch (lhs.value, rhs.value) {
+        case is (Void, Void):
+            return true
+        case let (lhs as Bool, rhs as Bool):
+            return lhs == rhs
+        case let (lhs as Int, rhs as Int):
+            return lhs == rhs
+        case let (lhs as Int8, rhs as Int8):
+            return lhs == rhs
+        case let (lhs as Int16, rhs as Int16):
+            return lhs == rhs
+        case let (lhs as Int32, rhs as Int32):
+            return lhs == rhs
+        case let (lhs as Int64, rhs as Int64):
+            return lhs == rhs
+        case let (lhs as UInt, rhs as UInt):
+            return lhs == rhs
+        case let (lhs as UInt8, rhs as UInt8):
+            return lhs == rhs
+        case let (lhs as UInt16, rhs as UInt16):
+            return lhs == rhs
+        case let (lhs as UInt32, rhs as UInt32):
+            return lhs == rhs
+        case let (lhs as UInt64, rhs as UInt64):
+            return lhs == rhs
+        case let (lhs as Decimal, rhs as Decimal):
+            return lhs == rhs
+        case let (lhs as Float, rhs as Float):
+            return lhs == rhs
+        case let (lhs as Double, rhs as Double):
+            return lhs == rhs
+        case let (lhs as String, rhs as String):
+            return lhs == rhs
+        case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]):
+            return lhs == rhs
+        case let (lhs as [AnyCodable], rhs as [AnyCodable]):
+            return lhs == rhs
+        default:
+            return false
+        }
+    }
+}
+
+extension AnyCodable: CustomStringConvertible {
+    public var description: String {
+        switch value {
+        case is Void:
+            return String(describing: nil as Any?)
+        case let value as CustomStringConvertible:
+            return value.description
+        default:
+            return String(describing: value)
+        }
+    }
+}
+
+extension AnyCodable: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        switch value {
+        case let value as CustomDebugStringConvertible:
+            return "AnyCodable(\(value.debugDescription))"
+        default:
+            return "AnyCodable(\(description))"
+        }
+    }
+}
+
+extension AnyCodable: ExpressibleByNilLiteral {}
+extension AnyCodable: ExpressibleByBooleanLiteral {}
+extension AnyCodable: ExpressibleByIntegerLiteral {}
+extension AnyCodable: ExpressibleByFloatLiteral {}
+extension AnyCodable: ExpressibleByStringLiteral {}
+extension AnyCodable: ExpressibleByArrayLiteral {}
+extension AnyCodable: ExpressibleByDictionaryLiteral {}

+ 167 - 0
FreeAPS/Sources/Helpers/AnyCodable/AnyDecodable.swift

@@ -0,0 +1,167 @@
+#if canImport(Foundation)
+    import Foundation
+#endif
+
+/**
+ A type-erased `Decodable` value.
+ 
+ The `AnyDecodable` type forwards decoding responsibilities
+ to an underlying value, hiding its specific underlying type.
+ 
+ You can decode mixed-type values in dictionaries
+ and other collections that require `Decodable` conformance
+ by declaring their contained type to be `AnyDecodable`:
+ 
+     let json = """
+     {
+         "boolean": true,
+         "integer": 42,
+         "double": 3.141592653589793,
+         "string": "string",
+         "array": [1, 2, 3],
+         "nested": {
+             "a": "alpha",
+             "b": "bravo",
+             "c": "charlie"
+         }
+     }
+     """.data(using: .utf8)!
+ 
+     let decoder = JSONDecoder()
+     let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json)
+ */
+#if swift(>=5.1)
+    @frozen public struct AnyDecodable: Decodable {
+        public let value: Any
+
+        public init<T>(_ value: T?) {
+            self.value = value ?? ()
+        }
+    }
+#else
+    public struct AnyDecodable: Decodable {
+        public let value: Any
+
+        public init<T>(_ value: T?) {
+            self.value = value ?? ()
+        }
+    }
+#endif
+
+#if swift(>=4.2)
+    public protocol _AnyDecodable {
+        var value: Any { get }
+        init<T>(_ value: T?)
+    }
+#else
+    protocol _AnyDecodable {
+        var value: Any { get }
+        init<T>(_ value: T?)
+    }
+#endif
+
+extension AnyDecodable: _AnyDecodable {}
+
+public extension _AnyDecodable {
+    init(from decoder: Decoder) throws {
+        let container = try decoder.singleValueContainer()
+
+        if container.decodeNil() {
+            #if canImport(Foundation)
+                self.init(NSNull())
+            #else
+                self.init(Self?.none)
+            #endif
+        } else if let bool = try? container.decode(Bool.self) {
+            self.init(bool)
+        } else if let int = try? container.decode(Int.self) {
+            self.init(int)
+        } else if let uint = try? container.decode(UInt.self) {
+            self.init(uint)
+        } else if let decimal = try? container.decode(Decimal.self) {
+            self.init(decimal)
+        } else if let double = try? container.decode(Double.self) {
+            self.init(double)
+        } else if let string = try? container.decode(String.self) {
+            self.init(string)
+        } else if let array = try? container.decode([AnyDecodable].self) {
+            self.init(array.map(\.value))
+        } else if let dictionary = try? container.decode([String: AnyDecodable].self) {
+            self.init(dictionary.mapValues { $0.value })
+        } else {
+            throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyDecodable value cannot be decoded")
+        }
+    }
+}
+
+extension AnyDecodable: Equatable {
+    public static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool {
+        switch (lhs.value, rhs.value) {
+        #if canImport(Foundation)
+            case is (NSNull, NSNull),
+                 is (Void, Void):
+                return true
+        #endif
+        case let (lhs as Bool, rhs as Bool):
+            return lhs == rhs
+        case let (lhs as Int, rhs as Int):
+            return lhs == rhs
+        case let (lhs as Int8, rhs as Int8):
+            return lhs == rhs
+        case let (lhs as Int16, rhs as Int16):
+            return lhs == rhs
+        case let (lhs as Int32, rhs as Int32):
+            return lhs == rhs
+        case let (lhs as Int64, rhs as Int64):
+            return lhs == rhs
+        case let (lhs as UInt, rhs as UInt):
+            return lhs == rhs
+        case let (lhs as UInt8, rhs as UInt8):
+            return lhs == rhs
+        case let (lhs as UInt16, rhs as UInt16):
+            return lhs == rhs
+        case let (lhs as UInt32, rhs as UInt32):
+            return lhs == rhs
+        case let (lhs as UInt64, rhs as UInt64):
+            return lhs == rhs
+        case let (lhs as Decimal, rhs as Decimal):
+            return lhs == rhs
+        case let (lhs as Float, rhs as Float):
+            return lhs == rhs
+        case let (lhs as Double, rhs as Double):
+            return lhs == rhs
+        case let (lhs as String, rhs as String):
+            return lhs == rhs
+        case let (lhs as [String: AnyDecodable], rhs as [String: AnyDecodable]):
+            return lhs == rhs
+        case let (lhs as [AnyDecodable], rhs as [AnyDecodable]):
+            return lhs == rhs
+        default:
+            return false
+        }
+    }
+}
+
+extension AnyDecodable: CustomStringConvertible {
+    public var description: String {
+        switch value {
+        case is Void:
+            return String(describing: nil as Any?)
+        case let value as CustomStringConvertible:
+            return value.description
+        default:
+            return String(describing: value)
+        }
+    }
+}
+
+extension AnyDecodable: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        switch value {
+        case let value as CustomDebugStringConvertible:
+            return "AnyDecodable(\(value.debugDescription))"
+        default:
+            return "AnyDecodable(\(description))"
+        }
+    }
+}

+ 284 - 0
FreeAPS/Sources/Helpers/AnyCodable/AnyEncodable.swift

@@ -0,0 +1,284 @@
+#if canImport(Foundation)
+    import Foundation
+#endif
+
+/**
+ A type-erased `Encodable` value.
+ 
+ The `AnyEncodable` type forwards encoding responsibilities
+ to an underlying value, hiding its specific underlying type.
+ 
+ You can encode mixed-type values in dictionaries
+ and other collections that require `Encodable` conformance
+ by declaring their contained type to be `AnyEncodable`:
+ 
+     let dictionary: [String: AnyEncodable] = [
+         "boolean": true,
+         "integer": 42,
+         "double": 3.141592653589793,
+         "string": "string",
+         "array": [1, 2, 3],
+         "nested": [
+             "a": "alpha",
+             "b": "bravo",
+             "c": "charlie"
+         ]
+     ]
+ 
+     let encoder = JSONEncoder()
+     let json = try! encoder.encode(dictionary)
+ */
+#if swift(>=5.1)
+    @frozen public struct AnyEncodable: Encodable {
+        public let value: Any
+
+        public init<T>(_ value: T?) {
+            self.value = value ?? ()
+        }
+    }
+#else
+    public struct AnyEncodable: Encodable {
+        public let value: Any
+
+        public init<T>(_ value: T?) {
+            self.value = value ?? ()
+        }
+    }
+#endif
+
+#if swift(>=4.2)
+    public protocol _AnyEncodable {
+        var value: Any { get }
+        init<T>(_ value: T?)
+    }
+#else
+    protocol _AnyEncodable {
+        var value: Any { get }
+        init<T>(_ value: T?)
+    }
+#endif
+
+extension AnyEncodable: _AnyEncodable {}
+
+// MARK: - Encodable
+
+extension _AnyEncodable {
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.singleValueContainer()
+
+        switch value {
+        #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
+            case let number as NSNumber:
+                try encode(nsnumber: number, into: &container)
+        #endif
+        #if canImport(Foundation)
+            case is NSNull:
+                try container.encodeNil()
+        #endif
+        case is Void:
+            try container.encodeNil()
+        case let bool as Bool:
+            try container.encode(bool)
+        case let int as Int:
+            try container.encode(int)
+        case let int8 as Int8:
+            try container.encode(int8)
+        case let int16 as Int16:
+            try container.encode(int16)
+        case let int32 as Int32:
+            try container.encode(int32)
+        case let int64 as Int64:
+            try container.encode(int64)
+        case let uint as UInt:
+            try container.encode(uint)
+        case let uint8 as UInt8:
+            try container.encode(uint8)
+        case let uint16 as UInt16:
+            try container.encode(uint16)
+        case let uint32 as UInt32:
+            try container.encode(uint32)
+        case let uint64 as UInt64:
+            try container.encode(uint64)
+        case let decimal as Decimal:
+            try container.encode(decimal)
+        case let float as Float:
+            try container.encode(float)
+        case let double as Double:
+            try container.encode(double)
+        case let string as String:
+            try container.encode(string)
+        #if canImport(Foundation)
+            case let date as Date:
+                try container.encode(date)
+            case let url as URL:
+                try container.encode(url)
+        #endif
+        case let array as [Any?]:
+            try container.encode(array.map { AnyEncodable($0) })
+        case let dictionary as [String: Any?]:
+            try container.encode(dictionary.mapValues { AnyEncodable($0) })
+        default:
+            let context = EncodingError.Context(
+                codingPath: container.codingPath,
+                debugDescription: "AnyEncodable value cannot be encoded"
+            )
+            throw EncodingError.invalidValue(value, context)
+        }
+    }
+
+    #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
+        private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws {
+            switch CFNumberGetType(nsnumber) {
+            case .charType:
+                try container.encode(nsnumber.boolValue)
+            case .sInt8Type:
+                try container.encode(nsnumber.int8Value)
+            case .sInt16Type:
+                try container.encode(nsnumber.int16Value)
+            case .sInt32Type:
+                try container.encode(nsnumber.int32Value)
+            case .sInt64Type:
+                try container.encode(nsnumber.int64Value)
+            case .shortType:
+                try container.encode(nsnumber.uint16Value)
+            case .longType:
+                try container.encode(nsnumber.uint32Value)
+            case .longLongType:
+                try container.encode(nsnumber.uint64Value)
+            case .cfIndexType,
+                 .intType,
+                 .nsIntegerType:
+                try container.encode(nsnumber.intValue)
+            case .float32Type,
+                 .floatType:
+                try container.encode(nsnumber.floatValue)
+            case .cgFloatType,
+                 .doubleType,
+                 .float64Type:
+                try container.encode(nsnumber.doubleValue)
+            #if swift(>=5.0)
+                @unknown default:
+                    let context = EncodingError.Context(
+                        codingPath: container.codingPath,
+                        debugDescription: "NSNumber cannot be encoded because its type is not handled"
+                    )
+                    throw EncodingError.invalidValue(nsnumber, context)
+            #endif
+            }
+        }
+    #endif
+}
+
+extension AnyEncodable: Equatable {
+    public static func == (lhs: AnyEncodable, rhs: AnyEncodable) -> Bool {
+        switch (lhs.value, rhs.value) {
+        case is (Void, Void):
+            return true
+        case let (lhs as Bool, rhs as Bool):
+            return lhs == rhs
+        case let (lhs as Int, rhs as Int):
+            return lhs == rhs
+        case let (lhs as Int8, rhs as Int8):
+            return lhs == rhs
+        case let (lhs as Int16, rhs as Int16):
+            return lhs == rhs
+        case let (lhs as Int32, rhs as Int32):
+            return lhs == rhs
+        case let (lhs as Int64, rhs as Int64):
+            return lhs == rhs
+        case let (lhs as UInt, rhs as UInt):
+            return lhs == rhs
+        case let (lhs as UInt8, rhs as UInt8):
+            return lhs == rhs
+        case let (lhs as UInt16, rhs as UInt16):
+            return lhs == rhs
+        case let (lhs as UInt32, rhs as UInt32):
+            return lhs == rhs
+        case let (lhs as UInt64, rhs as UInt64):
+            return lhs == rhs
+        case let (lhs as Decimal, rhs as Decimal):
+            return lhs == rhs
+        case let (lhs as Float, rhs as Float):
+            return lhs == rhs
+        case let (lhs as Double, rhs as Double):
+            return lhs == rhs
+        case let (lhs as String, rhs as String):
+            return lhs == rhs
+        case let (lhs as [String: AnyEncodable], rhs as [String: AnyEncodable]):
+            return lhs == rhs
+        case let (lhs as [AnyEncodable], rhs as [AnyEncodable]):
+            return lhs == rhs
+        default:
+            return false
+        }
+    }
+}
+
+extension AnyEncodable: CustomStringConvertible {
+    public var description: String {
+        switch value {
+        case is Void:
+            return String(describing: nil as Any?)
+        case let value as CustomStringConvertible:
+            return value.description
+        default:
+            return String(describing: value)
+        }
+    }
+}
+
+extension AnyEncodable: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        switch value {
+        case let value as CustomDebugStringConvertible:
+            return "AnyEncodable(\(value.debugDescription))"
+        default:
+            return "AnyEncodable(\(description))"
+        }
+    }
+}
+
+extension AnyEncodable: ExpressibleByNilLiteral {}
+extension AnyEncodable: ExpressibleByBooleanLiteral {}
+extension AnyEncodable: ExpressibleByIntegerLiteral {}
+extension AnyEncodable: ExpressibleByFloatLiteral {}
+extension AnyEncodable: ExpressibleByStringLiteral {}
+#if swift(>=5.0)
+    extension AnyEncodable: ExpressibleByStringInterpolation {}
+#endif
+extension AnyEncodable: ExpressibleByArrayLiteral {}
+extension AnyEncodable: ExpressibleByDictionaryLiteral {}
+
+public extension _AnyEncodable {
+    init(nilLiteral _: ()) {
+        self.init(nil as Any?)
+    }
+
+    init(booleanLiteral value: Bool) {
+        self.init(value)
+    }
+
+    init(integerLiteral value: Int) {
+        self.init(value)
+    }
+
+    init(floatLiteral value: Double) {
+        self.init(value)
+    }
+
+    init(extendedGraphemeClusterLiteral value: String) {
+        self.init(value)
+    }
+
+    init(stringLiteral value: String) {
+        self.init(value)
+    }
+
+    init(arrayLiteral elements: Any...) {
+        self.init(elements)
+    }
+
+    init(dictionaryLiteral elements: (AnyHashable, Any)...) {
+        self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first }))
+    }
+}

+ 17 - 6
FreeAPS/Sources/Helpers/JSON.swift

@@ -1,18 +1,23 @@
 import Foundation
 import Foundation
 
 
 protocol JSON: Codable {
 protocol JSON: Codable {
-    var string: String { get }
+    var rawJSON: String { get }
     init?(from: String)
     init?(from: String)
 }
 }
 
 
 extension JSON {
 extension JSON {
-    var string: String {
-        String(data: try! JSONEncoder().encode(self), encoding: .utf8)!
+    var rawJSON: RawJSON {
+        let encoder = JSONEncoder()
+        encoder.outputFormatting = .prettyPrinted
+        encoder.dateEncodingStrategy = .iso8601
+        return String(data: try! encoder.encode(self), encoding: .utf8)!
     }
     }
 
 
     init?(from: String) {
     init?(from: String) {
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .iso8601
         guard let data = from.data(using: .utf8),
         guard let data = from.data(using: .utf8),
-              let object = try? JSONDecoder().decode(Self.self, from: data)
+              let object = try? decoder.decode(Self.self, from: data)
         else {
         else {
             return nil
             return nil
         }
         }
@@ -21,7 +26,7 @@ extension JSON {
 }
 }
 
 
 extension String: JSON {
 extension String: JSON {
-    var string: String { self }
+    var rawJSON: String { self }
     init?(from: String) { self = from }
     init?(from: String) { self = from }
 }
 }
 
 
@@ -32,7 +37,7 @@ extension Int: JSON {}
 extension Bool: JSON {}
 extension Bool: JSON {}
 
 
 extension Date: JSON {
 extension Date: JSON {
-    var string: String {
+    var rawJSON: String {
         let formatter = ISO8601DateFormatter()
         let formatter = ISO8601DateFormatter()
         return formatter.string(from: self)
         return formatter.string(from: self)
     }
     }
@@ -50,3 +55,9 @@ extension Date: JSON {
 }
 }
 
 
 typealias RawJSON = String
 typealias RawJSON = String
+typealias AnyJSON = AnyCodable
+
+extension AnyJSON: JSON {}
+
+extension Array: JSON where Element: JSON {}
+extension Dictionary: JSON where Key: JSON, Value: JSON {}

+ 5 - 3
FreeAPS/Sources/Models/PumpHystoryEvent.swift

@@ -1,7 +1,6 @@
 import Foundation
 import Foundation
 
 
 struct PumpHystoryEvent: JSON {
 struct PumpHystoryEvent: JSON {
-    let id: UUID
     let type: PumpHystoryEventType
     let type: PumpHystoryEventType
     let timestamp: Date
     let timestamp: Date
     let amount: Decimal?
     let amount: Decimal?
@@ -16,9 +15,13 @@ enum PumpHystoryEventType: String, JSON {
     case mealBulus = "Meal Bolus"
     case mealBulus = "Meal Bolus"
     case correctionBolus = "Correction Bolus"
     case correctionBolus = "Correction Bolus"
     case snackBolus = "Snack Bolus"
     case snackBolus = "Snack Bolus"
-    case bolusWizard = "Bolus Wizard"
+    case bolusWizard = "BolusWizard"
     case tempBasal = "TempBasal"
     case tempBasal = "TempBasal"
     case tempBasalDuration = "TempBasalDuration"
     case tempBasalDuration = "TempBasalDuration"
+    case pumpSuspend = "PumpSuspend"
+    case pumpResume = "PumpResume"
+    case rewind = "Rewind"
+    case prime = "Prime"
 }
 }
 
 
 enum PumpHystoryTempType: String, JSON {
 enum PumpHystoryTempType: String, JSON {
@@ -28,7 +31,6 @@ enum PumpHystoryTempType: String, JSON {
 
 
 extension PumpHystoryEvent {
 extension PumpHystoryEvent {
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
-        case id
         case type = "_type"
         case type = "_type"
         case timestamp
         case timestamp
         case amount
         case amount

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

@@ -3,7 +3,12 @@ extension ConfigEditor {
         @Injected() private var storage: FileStorage!
         @Injected() private var storage: FileStorage!
 
 
         func load(file: String) -> RawJSON {
         func load(file: String) -> RawJSON {
-            (try? storage.retrieve(file, as: RawJSON.self)) ?? defaults(for: file)
+            if let value = try? storage.retrieve(file, as: RawJSON.self) {
+                return value
+            } else if let value = try? storage.retrieve(file, as: [AnyJSON].self) {
+                return value.rawJSON
+            }
+            return defaults(for: file)
         }
         }
 
 
         func save(_ value: RawJSON, as file: String) {
         func save(_ value: RawJSON, as file: String) {

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

@@ -8,6 +8,7 @@ extension Settings {
             Form {
             Form {
                 Text("Preferences").modal(for: .configEditor(file: OpenAPS.Settings.preferences), from: self)
                 Text("Preferences").modal(for: .configEditor(file: OpenAPS.Settings.preferences), from: self)
                 Text("Autosense").modal(for: .configEditor(file: OpenAPS.Settings.autosense), from: self)
                 Text("Autosense").modal(for: .configEditor(file: OpenAPS.Settings.autosense), from: self)
+                Text("Pump History").modal(for: .configEditor(file: OpenAPS.Monitor.pumpHistory), from: self)
                 Text("Nightscout").modal(for: .nighscoutConfig, from: self)
                 Text("Nightscout").modal(for: .nighscoutConfig, from: self)
                 Text("Pump").modal(for: .pumpConfig, from: self)
                 Text("Pump").modal(for: .pumpConfig, from: self)
             }
             }

+ 46 - 4
FreeAPS/Sources/Services/Storage/FileStorage.swift

@@ -10,15 +10,27 @@ protocol FileStorage {
     func retrievePublisher<Value: JSON>(_: String, as type: Value.Type) -> AnyPublisher<Value, Error>
     func retrievePublisher<Value: JSON>(_: String, as type: Value.Type) -> AnyPublisher<Value, Error>
 
 
     func append<Value: JSON>(_ newValue: Value, to name: String) throws
     func append<Value: JSON>(_ newValue: Value, to name: String) throws
+    func append<Value: JSON>(_ newValue: [Value], to name: String) throws
     func appendPublisher<Value: JSON>(_: Value, to name: String) -> AnyPublisher<Void, Error>
     func appendPublisher<Value: JSON>(_: Value, to name: String) -> AnyPublisher<Void, Error>
+    func appendPublisher<Value: JSON>(_ newValue: [Value], to name: String) -> AnyPublisher<Void, Error>
 }
 }
 
 
 final class BaseFileStorage: FileStorage {
 final class BaseFileStorage: FileStorage {
     private let processQueue = DispatchQueue(label: "BaseFileStorage.processQueue")
     private let processQueue = DispatchQueue(label: "BaseFileStorage.processQueue")
-
-    func save<Value: JSON>(_ value: Value, as name: String) throws {
+    private var encoder: JSONEncoder {
         let encoder = JSONEncoder()
         let encoder = JSONEncoder()
         encoder.outputFormatting = .prettyPrinted
         encoder.outputFormatting = .prettyPrinted
+        encoder.dateEncodingStrategy = .iso8601
+        return encoder
+    }
+
+    private var decoder: JSONDecoder {
+        let decoder = JSONDecoder()
+        decoder.dateDecodingStrategy = .iso8601
+        return decoder
+    }
+
+    func save<Value: JSON>(_ value: Value, as name: String) throws {
         try Disk.save(value, to: .documents, as: name, encoder: encoder)
         try Disk.save(value, to: .documents, as: name, encoder: encoder)
     }
     }
 
 
@@ -37,7 +49,7 @@ final class BaseFileStorage: FileStorage {
     }
     }
 
 
     func retrieve<Value: JSON>(_ name: String, as type: Value.Type) throws -> Value {
     func retrieve<Value: JSON>(_ name: String, as type: Value.Type) throws -> Value {
-        try Disk.retrieve(name, from: .documents, as: type)
+        try Disk.retrieve(name, from: .documents, as: type, decoder: decoder)
     }
     }
 
 
     func retrievePublisher<Value: JSON>(_ name: String, as type: Value.Type) -> AnyPublisher<Value, Error> {
     func retrievePublisher<Value: JSON>(_ name: String, as type: Value.Type) -> AnyPublisher<Value, Error> {
@@ -55,7 +67,11 @@ final class BaseFileStorage: FileStorage {
     }
     }
 
 
     func append<Value: JSON>(_ newValue: Value, to name: String) throws {
     func append<Value: JSON>(_ newValue: Value, to name: String) throws {
-        try Disk.append(newValue, to: name, in: .documents)
+        try Disk.append(newValue, to: name, in: .documents, encoder: encoder)
+    }
+
+    func append<Value: JSON>(_ newValue: [Value], to name: String) throws {
+        try Disk.append(newValue, to: name, in: .documents, encoder: encoder)
     }
     }
 
 
     func appendPublisher<Value: JSON>(_ newValue: Value, to name: String) -> AnyPublisher<Void, Error> {
     func appendPublisher<Value: JSON>(_ newValue: Value, to name: String) -> AnyPublisher<Void, Error> {
@@ -71,4 +87,30 @@ final class BaseFileStorage: FileStorage {
         }
         }
         .eraseToAnyPublisher()
         .eraseToAnyPublisher()
     }
     }
+
+    func appendPublisher<Value: JSON>(_ newValue: [Value], to name: String) -> AnyPublisher<Void, Error> {
+        Future { promise in
+            self.processQueue.async {
+                do { func appendPublisher<Value: JSON>(_ newValue: Value, to name: String) -> AnyPublisher<Void, Error> {
+                    Future { promise in
+                        self.processQueue.async {
+                            do {
+                                try self.append(newValue, to: name)
+                                promise(.success(()))
+                            } catch {
+                                promise(.failure(error))
+                            }
+                        }
+                    }
+                    .eraseToAnyPublisher()
+                }
+                try self.append(newValue, to: name)
+                promise(.success(()))
+                } catch {
+                    promise(.failure(error))
+                }
+            }
+        }
+        .eraseToAnyPublisher()
+    }
 }
 }