Przeglądaj źródła

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

Jon B.M 4 lat temu
rodzic
commit
89df549aec
30 zmienionych plików z 694 dodań i 76 usunięć
  1. 107 2
      FreeAPS.xcodeproj/project.pbxproj
  2. 9 0
      FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
  3. 6 1
      FreeAPS/Resources/FreeAPS.entitlements
  4. 13 0
      FreeAPS/Resources/Info.plist
  5. 1 0
      FreeAPS/Sources/APS/CGM/AppGroupSource.swift
  6. 5 0
      FreeAPS/Sources/APS/CGM/CGMType.swift
  7. 105 0
      FreeAPS/Sources/APS/CGM/Calibrations/CalibrationService.swift
  8. 2 0
      FreeAPS/Sources/APS/CGM/DexcomSource.swift
  9. 104 0
      FreeAPS/Sources/APS/CGM/LibreTransmitterSource.swift
  10. 7 0
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  11. 1 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  12. 1 1
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  13. 2 0
      FreeAPS/Sources/Assemblies/APSAssembly.swift
  14. 58 0
      FreeAPS/Sources/Helpers/HKUnit.swift
  15. 2 1
      FreeAPS/Sources/Models/BloodGlucose.swift
  16. 3 1
      FreeAPS/Sources/Modules/CGM/CGMStateModel.swift
  17. 7 0
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  18. 5 0
      FreeAPS/Sources/Modules/Calibrations/CalibrationsDataFlow.swift
  19. 3 0
      FreeAPS/Sources/Modules/Calibrations/CalibrationsProvider.swift
  20. 60 0
      FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  21. 44 0
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift
  22. 77 0
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  23. 2 0
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  24. 5 0
      FreeAPS/Sources/Modules/LibreConfig/LibreConfigDataFlow.swift
  25. 3 0
      FreeAPS/Sources/Modules/LibreConfig/LibreConfigProvider.swift
  26. 18 0
      FreeAPS/Sources/Modules/LibreConfig/LibreConfigStateModel.swift
  27. 36 0
      FreeAPS/Sources/Modules/LibreConfig/View/LibreConfigRootView.swift
  28. 2 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  29. 6 0
      FreeAPS/Sources/Router/Screen.swift
  30. 0 70
      FreeAPSTests/FileStorageTests.swift

+ 107 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -18,6 +18,7 @@
 		28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86FC1CFD647CF34508AF9A3B /* AddCarbsRootView.swift */; };
 		2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */; };
 		3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */; };
+		320D030F724170A637F06D50 /* CalibrationsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */; };
 		33E198D3039045D98C3DC5D4 /* AddCarbsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E7C997E56DAF8D28D09014 /* AddCarbsStateModel.swift */; };
 		3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0725C9D32E00A708ED /* BaseView.swift */; };
 		3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0825C9D32F00A708ED /* BaseProvider.swift */; };
@@ -77,6 +78,8 @@
 		385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */; };
 		385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC025F2EA52002D6D5B /* Announcement.swift */; };
 		385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */; };
+		3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC04273D152B00BF832C /* CalibrationService.swift */; };
+		3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */; };
 		386A124C271704DA00DDC61C /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; };
 		386A124D271704DA00DDC61C /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		386A124F271707F000DDC61C /* DexcomSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386A124E271707F000DDC61C /* DexcomSource.swift */; };
@@ -180,6 +183,9 @@
 		38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3F92737E42000574A46 /* BaseStateModel.swift */; };
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
 		38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMProvider.swift */; };
+		38FEF408273B011A00574A46 /* LibreTransmitterSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */; };
+		38FEF40F273B2BDA00574A46 /* LibreTransmitter in Frameworks */ = {isa = PBXBuildFile; productRef = 38FEF40E273B2BDA00574A46 /* LibreTransmitter */; };
+		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A965332F237348B119FB858 /* PreferencesEditorRootView.swift */; };
 		448B6FCB252BD4796E2960C0 /* PumpSettingsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0274EE6439B1C3ED70730D41 /* PumpSettingsEditorDataFlow.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
@@ -188,12 +194,14 @@
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
 		5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE53A13D26F101B332EFFC8 /* AddTempTargetProvider.swift */; };
 		5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */; };
+		61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */; };
 		63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */; };
 		642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */; };
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
 		69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */; };
 		6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */; };
+		6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
@@ -202,6 +210,8 @@
 		891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF98E22A39CD656A230704 /* AutotuneConfigProvider.swift */; };
 		8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */; };
 		8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */; };
+		903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */; };
+		9050F378F0063C064D7FFC86 /* LibreConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */; };
 		919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C3B5FD881CA45DFDEE0EDA9 /* AddTempTargetStateModel.swift */; };
 		9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */; };
 		9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A48AE3AC813A49A517846A /* NightscoutConfigStateModel.swift */; };
@@ -212,7 +222,9 @@
 		A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E22146D3DF4853786C78132 /* CREditorDataFlow.swift */; };
 		A6F097A14CAAE0CE0D11BE1B /* AddCarbsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618E62C9757B2F95431B5DC0 /* AddCarbsProvider.swift */; };
 		AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */; };
+		B7C465E9472624D8A2BE2A6A /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
+		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
@@ -230,6 +242,7 @@
 		E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0127368630002FF094 /* APSAssembly.swift */; };
 		E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0227368630002FF094 /* NetworkAssembly.swift */; };
 		E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; };
+		E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */; };
 		E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; };
 		E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */; };
 		E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F48C3AC770D4CCD0EA2B0C2 /* AddCarbsDataFlow.swift */; };
@@ -302,6 +315,7 @@
 		198377E4266C13D2004DE65E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
 		199732B4271B72DD00129A3F /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		199732B5271B9EE900129A3F /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
+		212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsProvider.swift; sourceTree = "<group>"; };
 		223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusStateModel.swift; sourceTree = "<group>"; };
 		2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigRootView.swift; sourceTree = "<group>"; };
 		2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -361,6 +375,8 @@
 		385CEA8125F23DFD002D6D5B /* NightscoutStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStatus.swift; sourceTree = "<group>"; };
 		385CEAC025F2EA52002D6D5B /* Announcement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = "<group>"; };
 		385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsStorage.swift; sourceTree = "<group>"; };
+		3862CC04273D152B00BF832C /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
+		3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsChart.swift; sourceTree = "<group>"; };
 		386A124B271704DA00DDC61C /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		386A124E271707F000DDC61C /* DexcomSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSource.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
@@ -456,6 +472,8 @@
 		38FEF3F92737E42000574A46 /* BaseStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FD2738083E00574A46 /* CGMProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMProvider.swift; sourceTree = "<group>"; };
+		38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTransmitterSource.swift; sourceTree = "<group>"; };
+		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		39E7C997E56DAF8D28D09014 /* AddCarbsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsStateModel.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
@@ -463,7 +481,9 @@
 		3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorDataFlow.swift; sourceTree = "<group>"; };
 		42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorProvider.swift; sourceTree = "<group>"; };
 		44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorProvider.swift; sourceTree = "<group>"; };
+		47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsStateModel.swift; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
+		500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
 		5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetDataFlow.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
@@ -472,6 +492,7 @@
 		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
 		618E62C9757B2F95431B5DC0 /* AddCarbsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorStateModel.swift; sourceTree = "<group>"; };
+		66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigDataFlow.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
 		680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = "<group>"; };
 		6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorProvider.swift; sourceTree = "<group>"; };
@@ -500,6 +521,7 @@
 		B8C7F882606FF83A21BE00D8 /* PumpSettingsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorRootView.swift; sourceTree = "<group>"; };
 		B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
+		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
@@ -508,12 +530,15 @@
 		D0BDC6993C1087310EDFC428 /* CREditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorRootView.swift; sourceTree = "<group>"; };
 		D295A3F870E826BE371C0BB5 /* AutotuneConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigStateModel.swift; sourceTree = "<group>"; };
 		D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorProvider.swift; sourceTree = "<group>"; };
+		DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		E00EEBFD27368630002FF094 /* ServiceAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFE27368630002FF094 /* SecurityAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurityAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFF27368630002FF094 /* StorageAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0027368630002FF094 /* UIAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0127368630002FF094 /* APSAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0227368630002FF094 /* NetworkAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = "<group>"; };
+		E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigProvider.swift; sourceTree = "<group>"; };
+		E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = "<group>"; };
 		E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = "<group>"; };
 		FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorRootView.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -523,6 +548,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				38FEF40F273B2BDA00574A46 /* LibreTransmitter in Frameworks */,
 				38887DF225F61F7500944304 /* NightscoutUploadKit.framework in Frameworks */,
 				38887DE825F61F7500944304 /* MockKitUI.framework in Frameworks */,
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
@@ -624,12 +650,14 @@
 				A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */,
 				3811DE0425C9D32E00A708ED /* Base */,
 				C2C98283C436DB934D7E7994 /* Bolus */,
+				E8176B120B55CE89F1591542 /* Calibrations */,
 				F75CB57ED6971B46F8756083 /* CGM */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E42231DBF0DBE2B4B92D1B15 /* CREditor */,
 				9E56E3626FAD933385101B76 /* DataTable */,
 				3811DE2725C9D49500A708ED /* Home */,
 				D8F047E14D567F2B5DBEFD96 /* ISFEditor */,
+				C11D545CED3ECEB525EDEE23 /* LibreConfig */,
 				3811DE1A25C9D48300A708ED /* Main */,
 				5031FE61F63C2A8A8B7674DD /* ManualTempBasal */,
 				D533BF261CDC1C3F871E7BFD /* NightscoutConfig */,
@@ -882,10 +910,20 @@
 				38569344270B5DFA0002C50D /* CGMType.swift */,
 				386A124E271707F000DDC61C /* DexcomSource.swift */,
 				38569345270B5DFA0002C50D /* GlucoseSource.swift */,
+				38FEF407273B011A00574A46 /* LibreTransmitterSource.swift */,
+				3862CC03273D150600BF832C /* Calibrations */,
 			);
 			path = CGM;
 			sourceTree = "<group>";
 		};
+		3862CC03273D150600BF832C /* Calibrations */ = {
+			isa = PBXGroup;
+			children = (
+				3862CC04273D152B00BF832C /* CalibrationService.swift */,
+			);
+			path = Calibrations;
+			sourceTree = "<group>";
+		};
 		3883582E25EEAFC000E024B2 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -972,6 +1010,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				38FEF412273B317A00574A46 /* HKUnit.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
@@ -1124,6 +1163,15 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		43952E72FE7AF85715FE020E /* View */ = {
+			isa = PBXGroup;
+			children = (
+				500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */,
+				3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		4E8C7B59F8065047ECE20965 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -1275,6 +1323,14 @@
 			path = BasalProfileEditor;
 			sourceTree = "<group>";
 		};
+		A56097CB1DCBCE98F2F42177 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		A9A4C88374496B3C89058A89 /* AddTempTarget */ = {
 			isa = PBXGroup;
 			children = (
@@ -1294,6 +1350,17 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		C11D545CED3ECEB525EDEE23 /* LibreConfig */ = {
+			isa = PBXGroup;
+			children = (
+				66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */,
+				E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */,
+				E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */,
+				A56097CB1DCBCE98F2F42177 /* View */,
+			);
+			path = LibreConfig;
+			sourceTree = "<group>";
+		};
 		C2C98283C436DB934D7E7994 /* Bolus */ = {
 			isa = PBXGroup;
 			children = (
@@ -1362,6 +1429,17 @@
 			path = PumpSettingsEditor;
 			sourceTree = "<group>";
 		};
+		E8176B120B55CE89F1591542 /* Calibrations */ = {
+			isa = PBXGroup;
+			children = (
+				DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */,
+				212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */,
+				47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */,
+				43952E72FE7AF85715FE020E /* View */,
+			);
+			path = Calibrations;
+			sourceTree = "<group>";
+		};
 		EEC747824D6593B5CD87E195 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -1405,6 +1483,7 @@
 				38B17B6525DD90E0005CAE3D /* SwiftDate */,
 				3833B46C26012030003021B3 /* Algorithms */,
 				38192E00261B826A0094D973 /* Alamofire */,
+				38FEF40E273B2BDA00574A46 /* LibreTransmitter */,
 			);
 			productName = FreeAPS;
 			productReference = 388E595825AD948C0019842D /* FreeAPS.app */;
@@ -1480,6 +1559,7 @@
 				38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */,
 				3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
 				38192DFF261B826A0094D973 /* XCRemoteSwiftPackageReference "Alamofire" */,
+				38FEF40D273B2BDA00574A46 /* XCRemoteSwiftPackageReference "LibreTransmitterX" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -1553,6 +1633,7 @@
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
+				38FEF408273B011A00574A46 /* LibreTransmitterSource.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				386A124F271707F000DDC61C /* DexcomSource.swift in Sources */,
@@ -1627,6 +1708,7 @@
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
+				3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */,
 				3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
@@ -1682,6 +1764,7 @@
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
 				17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */,
@@ -1712,6 +1795,7 @@
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
+				3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */,
 				711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
@@ -1728,6 +1812,14 @@
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
 				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
+				61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */,
+				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
+				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,
+				9050F378F0063C064D7FFC86 /* LibreConfigRootView.swift in Sources */,
+				B7C465E9472624D8A2BE2A6A /* CalibrationsDataFlow.swift in Sources */,
+				320D030F724170A637F06D50 /* CalibrationsProvider.swift in Sources */,
+				E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */,
+				BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -1905,7 +1997,7 @@
 		388E596825AD948E0019842D /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				APP_GROUP_ID = "group.$(PRODUCT_BUNDLE_IDENTIFIER)";
+				APP_GROUP_ID = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
@@ -1931,7 +2023,7 @@
 		388E596925AD948E0019842D /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
-				APP_GROUP_ID = "group.$(PRODUCT_BUNDLE_IDENTIFIER)";
+				APP_GROUP_ID = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
@@ -2069,6 +2161,14 @@
 				minimumVersion = 6.3.1;
 			};
 		};
+		38FEF40D273B2BDA00574A46 /* XCRemoteSwiftPackageReference "LibreTransmitterX" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/ivalkou/LibreTransmitterX";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.0.0;
+			};
+		};
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
@@ -2097,6 +2197,11 @@
 			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;
 			productName = SwiftDate;
 		};
+		38FEF40E273B2BDA00574A46 /* LibreTransmitter */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 38FEF40D273B2BDA00574A46 /* XCRemoteSwiftPackageReference "LibreTransmitterX" */;
+			productName = LibreTransmitter;
+		};
 /* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 388E595025AD948C0019842D /* Project object */;

+ 9 - 0
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -20,6 +20,15 @@
         }
       },
       {
+        "package": "LibreTransmitter",
+        "repositoryURL": "https://github.com/ivalkou/LibreTransmitterX",
+        "state": {
+          "branch": null,
+          "revision": "97301803d0c9be325d5c31c0f0ecbc3c2c24a1da",
+          "version": "1.0.5"
+        }
+      },
+      {
         "package": "swift-algorithms",
         "repositoryURL": "https://github.com/apple/swift-algorithms",
         "state": {

+ 6 - 1
FreeAPS/Resources/FreeAPS.entitlements

@@ -2,9 +2,14 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>com.apple.developer.nfc.readersession.formats</key>
+	<array>
+		<string>NDEF</string>
+		<string>TAG</string>
+	</array>
 	<key>com.apple.security.application-groups</key>
 	<array>
-		<string>$(APP_GROUP_ID)</string>
+		<string>group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup</string>
 	</array>
 </dict>
 </plist>

+ 13 - 0
FreeAPS/Resources/Info.plist

@@ -18,6 +18,17 @@
 	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
 	<key>CFBundleShortVersionString</key>
 	<string>$(MARKETING_VERSION)</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>freeaps-x</string>
+			</array>
+		</dict>
+	</array>
 	<key>CFBundleVersion</key>
 	<string>$(BUILD_VERSION)</string>
 	<key>ITSAppUsesNonExemptEncryption</key>
@@ -52,6 +63,8 @@
 	</dict>
 	<key>UIApplicationSupportsIndirectInputEvents</key>
 	<true/>
+	<key>NFCReaderUsageDescription</key>
+	<string>NFC is used to scan Libre sensors.</string>
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>bluetooth-central</string>

+ 1 - 0
FreeAPS/Sources/APS/CGM/AppGroupSource.swift

@@ -37,6 +37,7 @@ struct AppGroupSource: GlucoseSource {
                     direction: BloodGlucose.Direction(rawValue: direction),
                     date: Decimal(Int(date.timeIntervalSince1970 * 1000)),
                     dateString: date,
+                    unfiltered: nil,
                     filtered: nil,
                     noise: nil,
                     glucose: glucose,

+ 5 - 0
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -7,6 +7,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     case xdrip
     case dexcomG6
     case dexcomG5
+    case libreTransmitter
 
     var displayName: String {
         switch self {
@@ -18,6 +19,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return "Dexcom G6"
         case .dexcomG5:
             return "Dexcom G5"
+        case .libreTransmitter:
+            return "Libre Transmitter"
         }
     }
 
@@ -31,6 +34,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return URL(string: "dexcomg6://")!
         case .dexcomG5:
             return URL(string: "dexcomgcgm://")!
+        case .libreTransmitter:
+            return URL(string: "freeaps-x://libre-transmitter")!
         }
     }
 }

+ 105 - 0
FreeAPS/Sources/APS/CGM/Calibrations/CalibrationService.swift

@@ -0,0 +1,105 @@
+import Foundation
+import Swinject
+
+struct Calibration: JSON, Hashable, Identifiable {
+    let x: Double
+    let y: Double
+    var date = Date()
+
+    static let zero = Calibration(x: 0, y: 0)
+
+    var id = UUID()
+}
+
+protocol CalibrationService {
+    var slope: Double { get }
+    var intercept: Double { get }
+    var calibrations: [Calibration] { get }
+
+    func addCalibration(_ calibration: Calibration)
+    func removeCalibration(_ calibration: Calibration)
+    func removeAllCalibrations()
+    func removeLast()
+
+    func calibrate(value: Double) -> Double
+}
+
+final class BaseCalibrationService: CalibrationService, Injectable {
+    private enum Config {
+        static let minSlope = 0.8
+        static let maxSlope = 1.25
+        static let minIntercept = -100.0
+        static let maxIntercept = 100.0
+    }
+
+    @Injected() var storage: FileStorage!
+
+    private(set) var calibrations: [Calibration] = [] {
+        didSet {
+            storage.save(calibrations, as: OpenAPS.FreeAPS.calibrations)
+        }
+    }
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        calibrations = storage.retrieve(OpenAPS.FreeAPS.calibrations, as: [Calibration].self) ?? []
+    }
+
+    var slope: Double {
+        guard calibrations.count >= 2 else {
+            return 1
+        }
+
+        let xs = calibrations.map(\.x)
+        let ys = calibrations.map(\.y)
+        let sum1 = average(multiply(xs, ys)) - average(xs) * average(ys)
+        let sum2 = average(multiply(xs, xs)) - pow(average(xs), 2)
+        let slope = sum1 / sum2
+
+        return min(max(slope, Config.minSlope), Config.maxSlope)
+    }
+
+    var intercept: Double {
+        guard calibrations.count >= 1 else {
+            return 0
+        }
+        let xs = calibrations.map(\.x)
+        let ys = calibrations.map(\.y)
+
+        let intercept = average(ys) - slope * average(xs)
+
+        return min(max(intercept, Config.minIntercept), Config.maxIntercept)
+    }
+
+    func calibrate(value: Double) -> Double {
+        linearRegression(value)
+    }
+
+    func addCalibration(_ calibration: Calibration) {
+        calibrations.append(calibration)
+    }
+
+    func removeCalibration(_ calibration: Calibration) {
+        calibrations.removeAll { $0 == calibration }
+    }
+
+    func removeAllCalibrations() {
+        calibrations.removeAll()
+    }
+
+    func removeLast() {
+        calibrations.removeLast()
+    }
+
+    private func average(_ input: [Double]) -> Double {
+        input.reduce(0, +) / Double(input.count)
+    }
+
+    private func multiply(_ a: [Double], _ b: [Double]) -> [Double] {
+        zip(a, b).map(*)
+    }
+
+    private func linearRegression(_ x: Double) -> Double {
+        intercept + slope * x
+    }
+}

+ 2 - 0
FreeAPS/Sources/APS/CGM/DexcomSource.swift

@@ -47,10 +47,12 @@ extension DexcomSource: TransmitterManagerDelegate {
             let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
 
             return BloodGlucose(
+                _id: glucose.syncIdentifier,
                 sgv: value,
                 direction: .init(trend: glucose.trend),
                 date: Decimal(Int(glucose.readDate.timeIntervalSince1970 * 1000)),
                 dateString: glucose.readDate,
+                unfiltered: nil,
                 filtered: nil,
                 noise: nil,
                 glucose: value,

+ 104 - 0
FreeAPS/Sources/APS/CGM/LibreTransmitterSource.swift

@@ -0,0 +1,104 @@
+import Combine
+import Foundation
+import LibreTransmitter
+import Swinject
+
+protocol LibreTransmitterSource: GlucoseSource {
+    var manager: LibreTransmitterManager? { get set }
+}
+
+final class BaseLibreTransmitterSource: LibreTransmitterSource, Injectable {
+    private let processQueue = DispatchQueue(label: "BaseLibreTransmitterSource.processQueue")
+
+    @Injected() var glucoseStorage: GlucoseStorage!
+    @Injected() var calibrationService: CalibrationService!
+
+    private var promise: Future<[BloodGlucose], Error>.Promise?
+
+    var manager: LibreTransmitterManager? {
+        didSet {
+            configured = manager != nil
+            manager?.cgmManagerDelegate = self
+        }
+    }
+
+    @Persisted(key: "LibreTransmitterManager.configured") private(set) var configured = false
+
+    init(resolver: Resolver) {
+        if configured {
+            manager = LibreTransmitterManager()
+            manager?.cgmManagerDelegate = self
+        }
+
+        injectServices(resolver)
+    }
+
+    func fetch() -> AnyPublisher<[BloodGlucose], Never> {
+        Future<[BloodGlucose], Error> { [weak self] promise in
+            self?.promise = promise
+        }
+        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
+        .replaceError(with: [])
+        .eraseToAnyPublisher()
+    }
+}
+
+extension BaseLibreTransmitterSource: LibreTransmitterManagerDelegate {
+    var queue: DispatchQueue { processQueue }
+
+    func startDateToFilterNewData(for _: LibreTransmitterManager) -> Date? {
+        glucoseStorage.syncDate()
+    }
+
+    func cgmManager(_ manager: LibreTransmitterManager, hasNew result: Result<[LibreGlucose], Error>) {
+        switch result {
+        case let .success(newGlucose):
+            let glucose = newGlucose.map { value -> BloodGlucose in
+                BloodGlucose(
+                    _id: value.syncId,
+                    sgv: Int(value.glucose),
+                    direction: manager.glucoseDisplay?.trendType
+                        .map { .init(trendType: $0) },
+                    date: Decimal(Int(value.startDate.timeIntervalSince1970 * 1000)),
+                    dateString: value.startDate,
+                    unfiltered: Decimal(value.unsmoothedGlucose),
+                    filtered: nil,
+                    noise: nil,
+                    glucose: Int(value.glucose),
+                    type: "sgv"
+                )
+            }
+
+            promise?(.success(glucose))
+
+        case let .failure(error):
+            warning(.service, "LibreTransmitter error:", error: error)
+            promise?(.failure(error))
+        }
+    }
+
+    func overcalibration(for _: LibreTransmitterManager) -> ((Double) -> (Double))? {
+        calibrationService.calibrate
+    }
+}
+
+extension BloodGlucose.Direction {
+    init(trendType: GlucoseTrend) {
+        switch trendType {
+        case .upUpUp:
+            self = .doubleUp
+        case .upUp:
+            self = .singleUp
+        case .up:
+            self = .fortyFiveUp
+        case .flat:
+            self = .flat
+        case .down:
+            self = .fortyFiveDown
+        case .downDown:
+            self = .singleDown
+        case .downDownDown:
+            self = .doubleDown
+        }
+    }
+}

+ 7 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -11,6 +11,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @Injected() var nightscoutManager: NightscoutManager!
     @Injected() var apsManager: APSManager!
     @Injected() var settingsManager: SettingsManager!
+    @Injected() var libreTransmitter: LibreTransmitterSource!
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
@@ -36,6 +37,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         case .nightscout,
              .none:
             glucoseSource = nightscoutManager
+        case .libreTransmitter:
+            glucoseSource = libreTransmitter
+        }
+
+        if settingsManager.settings.cgm != .libreTransmitter {
+            libreTransmitter.manager = nil
         }
     }
 

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

@@ -84,5 +84,6 @@ extension OpenAPS {
         static let announcements = "freeaps/announcements.json"
         static let announcementsEnacted = "freeaps/announcements_enacted.json"
         static let tempTargetsPresets = "freeaps/temptargets_presets.json"
+        static let calibrations = "freeaps/calibrations.json"
     }
 }

+ 1 - 1
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -19,7 +19,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
 
     private enum Config {
-        static let filterTime: TimeInterval = 4.75 * 60
+        static let filterTime: TimeInterval = 4.5 * 60
     }
 
     init(resolver: Resolver) {

+ 2 - 0
FreeAPS/Sources/Assemblies/APSAssembly.swift

@@ -3,6 +3,8 @@ import Swinject
 
 final class APSAssembly: Assembly {
     func assemble(container: Container) {
+        container.register(CalibrationService.self) { r in BaseCalibrationService(resolver: r) }
+        container.register(LibreTransmitterSource.self) { r in BaseLibreTransmitterSource(resolver: r) }
         container.register(DeviceDataManager.self) { r in BaseDeviceDataManager(resolver: r) }
         container.register(APSManager.self) { r in BaseAPSManager(resolver: r) }
         container.register(FetchGlucoseManager.self) { r in BaseFetchGlucoseManager(resolver: r) }

+ 58 - 0
FreeAPS/Sources/Helpers/HKUnit.swift

@@ -0,0 +1,58 @@
+import HealthKit
+
+extension HKUnit {
+    static let milligramsPerDeciliter: HKUnit = {
+        HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
+    }()
+
+    static let millimolesPerLiter: HKUnit = {
+        HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
+    }()
+
+    static let internationalUnitsPerHour: HKUnit = {
+        HKUnit.internationalUnit().unitDivided(by: .hour())
+    }()
+
+    static let gramsPerUnit: HKUnit = {
+        HKUnit.gram().unitDivided(by: .internationalUnit())
+    }()
+
+    var foundationUnit: Unit? {
+        if self == HKUnit.milligramsPerDeciliter {
+            return UnitConcentrationMass.milligramsPerDeciliter
+        }
+
+        if self == HKUnit.millimolesPerLiter {
+            return UnitConcentrationMass.millimolesPerLiter(withGramsPerMole: HKUnitMolarMassBloodGlucose)
+        }
+
+        if self == HKUnit.gram() {
+            return UnitMass.grams
+        }
+
+        return nil
+    }
+
+    /// The smallest value expected to be visible on a chart
+    var chartableIncrement: Double {
+        if self == .milligramsPerDeciliter {
+            return 1
+        } else {
+            return 1 / 25
+        }
+    }
+
+    var localizedShortUnitString: String {
+        if self == HKUnit.millimolesPerLiter {
+            return NSLocalizedString("mmol/L", comment: "The short unit display string for millimoles of glucose per liter")
+        } else if self == .milligramsPerDeciliter {
+            return NSLocalizedString("mg/dL", comment: "The short unit display string for milligrams of glucose per decilter")
+        } else if self == .internationalUnit() {
+            return NSLocalizedString("U", comment: "The short unit display string for international units of insulin")
+        } else if self == .gram() {
+            return NSLocalizedString("g", comment: "The short unit display string for grams")
+        } else {
+            return String(describing: self)
+        }
+    }
+}

+ 2 - 1
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -22,9 +22,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
     }
 
     var sgv: Int?
-    let direction: Direction?
+    var direction: Direction?
     let date: Decimal
     let dateString: Date
+    let unfiltered: Decimal?
     let filtered: Decimal?
     let noise: Int?
     var glucose: Int?

+ 3 - 1
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -3,6 +3,7 @@ import SwiftUI
 extension CGM {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var settingsManager: SettingsManager!
+        @Injected() var libreSource: LibreTransmitterSource!
 
         @Published var cgm: CGMType = .nightscout
         @Published var transmitterID = ""
@@ -16,7 +17,8 @@ extension CGM {
             $cgm
                 .removeDuplicates()
                 .sink { [weak self] value in
-                    self?.settingsManager.settings.cgm = value
+                    guard let self = self else { return }
+                    self.settingsManager.settings.cgm = value
                 }
                 .store(in: &lifetime)
 

+ 7 - 0
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -30,6 +30,13 @@ extension CGM {
                     }
                 }
 
+                if state.cgm == .libreTransmitter {
+                    Button("Configure Libre Transmitter") {
+                        state.showModal(for: .libreConfig)
+                    }
+                    Text("Calibrations").navigationLink(to: .calibrations, from: self)
+                }
+
                 Section(header: Text("Other")) {
                     Toggle("Upload glucose to Nightscout", isOn: $state.uploadGlucose)
                 }

+ 5 - 0
FreeAPS/Sources/Modules/Calibrations/CalibrationsDataFlow.swift

@@ -0,0 +1,5 @@
+enum Calibrations {
+    enum Config {}
+}
+
+protocol CalibrationsProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/Calibrations/CalibrationsProvider.swift

@@ -0,0 +1,3 @@
+extension Calibrations {
+    final class Provider: BaseProvider, CalibrationsProvider {}
+}

+ 60 - 0
FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -0,0 +1,60 @@
+import SwiftDate
+import SwiftUI
+
+extension Calibrations {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var glucoseStorage: GlucoseStorage!
+        @Injected() var calibrationService: CalibrationService!
+        @Injected() var settingsManager: SettingsManager!
+
+        @Published var slope: Double = 1
+        @Published var intercept: Double = 1
+        @Published var newCalibration: Decimal = 0
+        @Published var calibrations: [Calibration] = []
+        @Published var calibrate: (Double) -> Double = { $0 }
+
+        var units: GlucoseUnits = .mmolL
+
+        override func subscribe() {
+            slope = calibrationService.slope
+            intercept = calibrationService.intercept
+
+            units = settingsManager.settings.units
+            calibrations = calibrationService.calibrations
+            calibrate = calibrationService.calibrate
+        }
+
+        func addCalibration() {
+            defer {
+                hideModal()
+            }
+
+            var glucose = newCalibration
+            if units == .mmolL {
+                glucose = newCalibration.asMgdL
+            }
+
+            guard let lastGlucose = glucoseStorage.recent().last,
+                  lastGlucose.dateString.addingTimeInterval(60 * 4.5) > Date(),
+                  let unfiltered = lastGlucose.unfiltered
+            else {
+                warning(.service, "Glucose is invalid for calibration")
+                return
+            }
+
+            let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
+
+            calibrationService.addCalibration(calibration)
+        }
+
+        func removeLast() {
+            calibrationService.removeLast()
+            hideModal()
+        }
+
+        func removeAll() {
+            calibrationService.removeAllCalibrations()
+            hideModal()
+        }
+    }
+}

+ 44 - 0
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift

@@ -0,0 +1,44 @@
+import SwiftUI
+
+struct CalibrationsChart: View {
+    @EnvironmentObject var state: Calibrations.StateModel
+
+    private let maxValue = 400.0
+
+    var body: some View {
+        GeometryReader { geo in
+            ZStack(alignment: .top) {
+                Rectangle().fill(Color.secondary)
+                    .frame(height: geo.size.width)
+                Path { path in
+                    let size = geo.size.width
+                    path.move(
+                        to:
+                        CGPoint(
+                            x: 0,
+                            y: size - state.calibrate(0) / maxValue * geo.size.width
+                        )
+                    )
+                    path.addLine(
+                        to: CGPoint(
+                            x: size,
+                            y: size - state.calibrate(maxValue) / maxValue * geo.size.width
+                        )
+                    )
+                }
+                .stroke(.blue, lineWidth: 2)
+
+                ForEach(state.calibrations, id: \.self) { value in
+                    Circle().fill(.red)
+                        .frame(width: 6, height: 6)
+                        .position(
+                            x: value.x / maxValue * geo.size.width,
+                            y: geo.size.width - (value.y / maxValue * geo.size.width)
+                        )
+                }
+            }
+            .frame(height: geo.size.width)
+            .clipped()
+        }
+    }
+}

+ 77 - 0
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -0,0 +1,77 @@
+import SwiftUI
+import Swinject
+
+extension Calibrations {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        var body: some View {
+            GeometryReader { geo in
+                Form {
+                    Section(header: Text("Add calibration")) {
+                        HStack {
+                            Text("Meter glucose")
+                            Spacer()
+                            DecimalTextField(
+                                "0",
+                                value: $state.newCalibration,
+                                formatter: formatter,
+                                autofocus: false,
+                                cleanInput: true
+                            )
+                            Text(state.units.rawValue).foregroundColor(.secondary)
+                        }
+                        Button {
+                            state.addCalibration()
+                        }
+                        label: { Text("Add") }
+                            .disabled(state.newCalibration <= 0)
+                    }
+
+                    Section(header: Text("Info")) {
+                        HStack {
+                            Text("Slope")
+                            Spacer()
+                            Text(formatter.string(from: state.slope as NSNumber)!)
+                        }
+                        HStack {
+                            Text("Intercept")
+                            Spacer()
+                            Text(formatter.string(from: state.intercept as NSNumber)!)
+                        }
+                    }
+
+                    Section(header: Text("Remove")) {
+                        Button {
+                            state.removeLast()
+                        }
+                        label: { Text("Remove Last") }
+                            .disabled(state.calibrations.isEmpty)
+
+                        Button {
+                            state.removeAll()
+                        }
+                        label: { Text("Remove All") }
+                            .disabled(state.calibrations.isEmpty)
+                    }
+
+                    Section(header: Text("Chart")) {
+                        CalibrationsChart().environmentObject(state)
+                            .frame(minHeight: geo.size.width)
+                    }
+                }
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Calibrations")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 2 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -274,6 +274,8 @@ extension Home {
                 url = URL(string: "spikeapp://")!
             case "http://127.0.0.1:17580":
                 url = URL(string: "diabox://")!
+            case CGMType.libreTransmitter.appURL?.absoluteString:
+                showModal(for: .libreConfig)
             default: break
             }
             UIApplication.shared.open(url, options: [:], completionHandler: nil)

+ 5 - 0
FreeAPS/Sources/Modules/LibreConfig/LibreConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum LibreConfig {
+    enum Config {}
+}
+
+protocol LibreConfigProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/LibreConfig/LibreConfigProvider.swift

@@ -0,0 +1,3 @@
+extension LibreConfig {
+    final class Provider: BaseProvider, LibreConfigProvider {}
+}

+ 18 - 0
FreeAPS/Sources/Modules/LibreConfig/LibreConfigStateModel.swift

@@ -0,0 +1,18 @@
+import HealthKit
+import SwiftUI
+
+extension LibreConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var source: LibreTransmitterSource!
+        @Injected() var settingsManager: SettingsManager!
+
+        @Published var configured = false
+
+        var unit = HKUnit.millimolesPerLiter
+
+        override func subscribe() {
+            configured = source.manager != nil
+            unit = settingsManager.settings.units == .mmolL ? .millimolesPerLiter : .milligramsPerDeciliter
+        }
+    }
+}

+ 36 - 0
FreeAPS/Sources/Modules/LibreConfig/View/LibreConfigRootView.swift

@@ -0,0 +1,36 @@
+import LibreTransmitter
+import SwiftUI
+import Swinject
+
+extension LibreConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        var body: some View {
+            Group {
+                if state.configured, let manager = state.source.manager {
+                    LibreTransmitterSettingsView(
+                        manager: manager,
+                        glucoseUnit: state.unit
+                    ) {
+                        self.state.source.manager = nil
+                        self.state.configured = false
+                    } completion: {
+                        state.hideModal()
+                    }
+                } else {
+                    LibreTransmitterSetupView { manager in
+                        self.state.source.manager = manager
+                        self.state.configured = true
+                    } completion: {
+                        state.hideModal()
+                    }
+                }
+            }
+            .navigationBarTitle("")
+            .navigationBarHidden(true)
+            .onAppear(perform: configureView)
+        }
+    }
+}

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

@@ -83,6 +83,8 @@ extension Settings {
                         Group {
                             Text("Target presets")
                                 .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self)
+                            Text("Calibrations")
+                                .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.calibrations), from: self)
                             Text("Middleware")
                                 .navigationLink(to: .configEditor(file: OpenAPS.Middleware.determineBasal), from: self)
                         }

+ 6 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -21,6 +21,8 @@ enum Screen: Identifiable, Hashable {
     case autotuneConfig
     case dataTable
     case cgm
+    case libreConfig
+    case calibrations
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -66,6 +68,10 @@ extension Screen {
             DataTable.RootView(resolver: resolver)
         case .cgm:
             CGM.RootView(resolver: resolver)
+        case .libreConfig:
+            LibreConfig.RootView(resolver: resolver)
+        case .calibrations:
+            Calibrations.RootView(resolver: resolver)
         }
     }
 

+ 0 - 70
FreeAPSTests/FileStorageTests.swift

@@ -16,74 +16,4 @@ class FileStorageTests: XCTestCase {
     override func tearDownWithError() throws {
         // Put teardown code here. This method is called after the invocation of each test method in the class.
     }
-
-    func testStorage() throws {
-        let uniqID = UUID().uuidString
-        let object1 = DummyObject(id: uniqID, value: 1.0)
-        let object2 = DummyObject(id: UUID().uuidString, value: 1.2)
-        let object3 = DummyObject(id: UUID().uuidString, value: 1.4)
-        let object4 = DummyObject(id: uniqID, value: 1.0)
-
-        do {
-            try fileStorage.save(object1, as: "tests/testStorage1.json")
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.save([object1, object2], as: "tests/testStorage2.json")
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            let value = try fileStorage.retrieve("tests/testStorage1.json", as: DummyObject.self)
-            XCTAssert(value.rawJSON == object1.rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            let values = try fileStorage.retrieve("tests/testStorage2.json", as: [DummyObject].self)
-            XCTAssert(values.rawJSON == [object1, object2].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.append(object3, to: "tests/testStorage1.json")
-            let values = try fileStorage.retrieve("tests/testStorage1.json", as: [DummyObject].self)
-
-            XCTAssert(values.rawJSON == [object1, object3].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.append([object2, object4], to: "tests/testStorage1.json")
-            let values = try fileStorage.retrieve("tests/testStorage1.json", as: [DummyObject].self)
-
-            XCTAssert(values.rawJSON == [object1, object3, object2, object4].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.append([object3, object4], to: "tests/testStorage2.json", uniqBy: \.id)
-            let values = try fileStorage.retrieve("tests/testStorage2.json", as: [DummyObject].self)
-
-            XCTAssert(values.rawJSON == [object1, object2, object3].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.remove("tests/testStorage1.json")
-            try fileStorage.rename("tests/testStorage2.json", to: "tests/testStorage1.json")
-            let values = try fileStorage.retrieve("tests/testStorage1.json", as: [DummyObject].self)
-            XCTAssert(values.rawJSON == [object1, object2, object3].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-    }
 }