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

New stat View (#33)

New Statistics View
Configure the BG limits in one place for entire app (in notifications) for charts, colour of  BG in header and for statistics.
Clean up.
Optional upload of statistics (and preferences) to NS once every 22 hours. 
Remove redundant settings.
Jon B Mårtensson 3 лет назад
Родитель
Сommit
a7e46d2b82
29 измененных файлов с 1507 добавлено и 1049 удалено
  1. 4 1
      Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  2. 40 8
      FreeAPS.xcodeproj/project.pbxproj
  3. 1 1
      FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
  4. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  5. 1 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  6. 561 572
      FreeAPS/Sources/APS/APSManager.swift
  7. 1 4
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  8. 3 2
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  9. 0 6
      FreeAPS/Sources/Models/Preferences.swift
  10. 8 0
      FreeAPS/Sources/Models/TIRforChart.swift
  11. 4 4
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  12. 2 4
      FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift
  13. 28 26
      FreeAPS/Sources/Modules/ConfigEditor/View/ConfigEditorRootView.swift
  14. 0 41
      FreeAPS/Sources/Modules/Home/DurationButton.swift
  15. 0 1
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  16. 0 8
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  17. 9 22
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  18. 20 21
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  19. 6 5
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  20. 16 264
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  21. 1 0
      FreeAPS/Sources/Modules/Main/MainStateModel.swift
  22. 0 51
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  23. 1 1
      FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift
  24. 5 0
      FreeAPS/Sources/Modules/Stat/StatDataFlow.swift
  25. 3 0
      FreeAPS/Sources/Modules/Stat/StatProvider.swift
  26. 21 0
      FreeAPS/Sources/Modules/Stat/StatStateModel.swift
  27. 767 0
      FreeAPS/Sources/Modules/Stat/View/StatRootView.swift
  28. 4 4
      FreeAPS/Sources/Router/Screen.swift
  29. 0 1
      FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

+ 4 - 1
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
@@ -63,6 +63,9 @@
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
     </entity>
+    <entity name="StatsData" representedClassName="StatsData" syncable="YES" codeGenerationType="class">
+        <attribute name="lastrun" optional="YES" attributeType="Date" defaultDateTimeInterval="704497620" usesScalarValueType="NO"/>
+    </entity>
     <entity name="Target" representedClassName="Target" syncable="YES" codeGenerationType="class">
         <attribute name="current" optional="YES" attributeType="Decimal" defaultValueString="100"/>
     </entity>

+ 40 - 8
FreeAPS.xcodeproj/project.pbxproj

@@ -21,13 +21,13 @@
 		1967DFC229D053D300759F30 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFC129D053D300759F30 /* IconImage.swift */; };
 		19795118275953E50044850D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 198377D4266BFFF6004DE65E /* Localizable.strings */; };
 		198377D2266BFFF6004DE65E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 198377D4266BFFF6004DE65E /* Localizable.strings */; };
-		19854F492961C3E500941627 /* DurationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19854F482961C3E500941627 /* DurationButton.swift */; };
 		199561C1275E61A50077B976 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 199561C0275E61A50077B976 /* HealthKit.framework */; };
 		19B0EF2128F6D66200069496 /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B0EF2028F6D66200069496 /* Statistics.swift */; };
 		19D466A329AA2B80004D5F33 /* FPUConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A229AA2B80004D5F33 /* FPUConfigDataFlow.swift */; };
 		19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A429AA2BD4004D5F33 /* FPUConfigProvider.swift */; };
 		19D466A729AA2C22004D5F33 /* FPUConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A629AA2C22004D5F33 /* FPUConfigStateModel.swift */; };
 		19D466AA29AA3099004D5F33 /* FPUConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A929AA3099004D5F33 /* FPUConfigRootView.swift */; };
+		19D4E4EB29FC6A9F00351451 /* TIRforChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */; };
 		19DA48E829CD339B00EEA1E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */; };
 		19DA48E929CD339C00EEA1E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */; };
 		19DA48EA29CD339C00EEA1E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */; };
@@ -39,6 +39,10 @@
 		19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E1F7E929D082ED005C8D20 /* IconConfigProvider.swift */; };
 		19E1F7EC29D082FE005C8D20 /* IconConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E1F7EB29D082FE005C8D20 /* IconConfigStateModel.swift */; };
 		19E1F7EF29D08EBA005C8D20 /* IconConfigRootWiew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E1F7EE29D08EBA005C8D20 /* IconConfigRootWiew.swift */; };
+		19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF229F10FBC00314DDC /* StatDataFlow.swift */; };
+		19F95FF529F10FCF00314DDC /* StatProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF429F10FCF00314DDC /* StatProvider.swift */; };
+		19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF629F10FEE00314DDC /* StatStateModel.swift */; };
+		19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF929F1102A00314DDC /* StatRootView.swift */; };
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */; };
 		1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* DataTableProvider.swift */; };
 		23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* BolusProvider.swift */; };
@@ -507,7 +511,6 @@
 		198377E2266C0AC8004DE65E /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
 		198377E3266C0ADC004DE65E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
 		198377E4266C13D2004DE65E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
-		19854F482961C3E500941627 /* DurationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationButton.swift; sourceTree = "<group>"; };
 		199561C0275E61A50077B976 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS8.0.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; };
 		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>"; };
@@ -518,6 +521,7 @@
 		19D466A429AA2BD4004D5F33 /* FPUConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigProvider.swift; sourceTree = "<group>"; };
 		19D466A629AA2C22004D5F33 /* FPUConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigStateModel.swift; sourceTree = "<group>"; };
 		19D466A929AA3099004D5F33 /* FPUConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigRootView.swift; sourceTree = "<group>"; };
+		19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIRforChart.swift; sourceTree = "<group>"; };
 		19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		19DC677E29CA675700FD9EC4 /* OverrideProfilesDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProfilesDataFlow.swift; sourceTree = "<group>"; };
 		19DC678029CA676A00FD9EC4 /* OverrideProfilesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProfilesProvider.swift; sourceTree = "<group>"; };
@@ -527,6 +531,10 @@
 		19E1F7E929D082ED005C8D20 /* IconConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconConfigProvider.swift; sourceTree = "<group>"; };
 		19E1F7EB29D082FE005C8D20 /* IconConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconConfigStateModel.swift; sourceTree = "<group>"; };
 		19E1F7EE29D08EBA005C8D20 /* IconConfigRootWiew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconConfigRootWiew.swift; sourceTree = "<group>"; };
+		19F95FF229F10FBC00314DDC /* StatDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatDataFlow.swift; sourceTree = "<group>"; };
+		19F95FF429F10FCF00314DDC /* StatProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatProvider.swift; sourceTree = "<group>"; };
+		19F95FF629F10FEE00314DDC /* StatStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatStateModel.swift; sourceTree = "<group>"; };
+		19F95FF929F1102A00314DDC /* StatRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatRootView.swift; sourceTree = "<group>"; };
 		1CAE81192B118804DCD23034 /* SnoozeProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeProvider.swift; 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>"; };
@@ -1037,6 +1045,25 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		19F95FF129F10F9C00314DDC /* Stat */ = {
+			isa = PBXGroup;
+			children = (
+				19F95FF229F10FBC00314DDC /* StatDataFlow.swift */,
+				19F95FF429F10FCF00314DDC /* StatProvider.swift */,
+				19F95FF629F10FEE00314DDC /* StatStateModel.swift */,
+				19F95FF829F10FF600314DDC /* View */,
+			);
+			path = Stat;
+			sourceTree = "<group>";
+		};
+		19F95FF829F10FF600314DDC /* View */ = {
+			isa = PBXGroup;
+			children = (
+				19F95FF929F1102A00314DDC /* StatRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		29B478DF61BF8D270F7D8954 /* Snooze */ = {
 			isa = PBXGroup;
 			children = (
@@ -1060,6 +1087,7 @@
 			isa = PBXGroup;
 			children = (
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
+				19F95FF129F10F9C00314DDC /* Stat */,
 				19E1F7E629D0828B005C8D20 /* IconConfig */,
 				19D466A129AA2B0A004D5F33 /* FPUConfig */,
 				F90692CD274B99850037068D /* HealthKit */,
@@ -1156,7 +1184,6 @@
 				3811DE2A25C9D49500A708ED /* HomeDataFlow.swift */,
 				3811DE2925C9D49500A708ED /* HomeProvider.swift */,
 				3811DE2825C9D49500A708ED /* HomeStateModel.swift */,
-				19854F482961C3E500941627 /* DurationButton.swift */,
 				3811DE2C25C9D49500A708ED /* View */,
 			);
 			path = Home;
@@ -1512,6 +1539,7 @@
 				FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */,
 				FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */,
 				1967DFBD29D052C200759F30 /* Icons.swift */,
+				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -2379,7 +2407,6 @@
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
-				19854F492961C3E500941627 /* DurationButton.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
@@ -2403,6 +2430,7 @@
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeStateModel.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
+				19F95FF529F10FCF00314DDC /* StatProvider.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
 				19B0EF2128F6D66200069496 /* Statistics.swift in Sources */,
 				3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */,
@@ -2469,6 +2497,7 @@
 				CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */,
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				CECA4775298DA8310095139F /* DexcomSourceG5.swift in Sources */,
+				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
@@ -2542,6 +2571,7 @@
 				38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
 				F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */,
+				19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */,
 				385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */,
 				8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */,
 				389442CB25F65F7100FA1F27 /* NightscoutTreatment.swift in Sources */,
@@ -2609,6 +2639,7 @@
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
+				19D4E4EB29FC6A9F00351451 /* TIRforChart.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
 				3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */,
@@ -2628,6 +2659,7 @@
 				A05235B9112E677ED03B6E8E /* AutotuneConfigRootView.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
 				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
+				19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */,
 				0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */,
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
@@ -2819,7 +2851,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 15.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
@@ -2878,7 +2910,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 15.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
 				MARKETING_VERSION = "$(APP_VERSION)";
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
@@ -2909,7 +2941,7 @@
 					"$(PROJECT_DIR)/Dependencies/ios-armv7_arm64",
 				);
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 15.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -2951,7 +2983,7 @@
 					"$(PROJECT_DIR)/Dependencies/ios-armv7_arm64",
 				);
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 15.2;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",

+ 1 - 1
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -30,7 +30,7 @@
       },
       {
         "package": "SwiftCharts",
-        "repositoryURL": "https://github.com/ivanschuetz/SwiftCharts",
+        "repositoryURL": "https://github.com/ivanschuetz/SwiftCharts.git",
         "state": {
           "branch": "master",
           "revision": "c354c1945bb35a1f01b665b22474f6db28cba4a2",

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


+ 1 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -17,7 +17,7 @@
     "useAlarmSound": false,
     "addSourceInfoToGlucoseNotifications": false,
     "lowGlucose": 72,
-    "highGlucose": 270,
+    "highGlucose": 145,
     "carbsRequiredThreshold": 10,
     "useAppleHealth": false,
     "animatedBackground": false,

Разница между файлами не показана из-за своего большого размера
+ 561 - 572
FreeAPS/Sources/APS/APSManager.swift


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

@@ -227,7 +227,7 @@ Enact a temp Basal or a temp target */
 /* */
 "Manual Temp Basal" = "Ручная ВБС";
 
-/* Allow uploads tp NS */
+/* Allow uploads data to NS */
 "Allow uploads" = "Разрешить выгрузку";
 
 /* API secret in NS */
@@ -236,9 +236,6 @@ Enact a temp Basal or a temp target */
 /* Connect to NS */
 "Connect" = "Подключить";
 
-/* Connected to NS */
-"Connected!" = "Подключено!";
-
 /* Connecting to NS */
 "Connecting..." = "Подключение...";
 

+ 3 - 2
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -16,12 +16,11 @@ struct FreeAPSSettings: JSON, Equatable {
     var cgm: CGMType = .nightscout
     var uploadGlucose: Bool = false
     var useCalendar: Bool = false
-    var useAppleHealth: Bool = false
     var glucoseBadge: Bool = false
     var glucoseNotificationsAlways: Bool = false
     var useAlarmSound: Bool = false
     var addSourceInfoToGlucoseNotifications: Bool = false
-    var lowGlucose: Decimal = 72
+    var lowGlucose: Decimal = 70
     var highGlucose: Decimal = 270
     var carbsRequiredThreshold: Decimal = 10
     var animatedBackground: Bool = false
@@ -31,7 +30,9 @@ struct FreeAPSSettings: JSON, Equatable {
     var timeCap: Int = 8
     var minuteInterval: Int = 30
     var delay: Int = 60
+    var useAppleHealth: Bool = false
     var smoothGlucose: Bool = false
+    var overrideHbA1cUnit: Bool = false
 }
 
 extension FreeAPSSettings: Decodable {

+ 0 - 6
FreeAPS/Sources/Models/Preferences.swift

@@ -52,11 +52,8 @@ struct Preferences: JSON {
     var enableSMB_high_bg: Bool = false
     var enableSMB_high_bg_target: Decimal = 110
     var threshold_setting: Decimal = 65
-    var high: Decimal = 10
-    var low: Decimal = 4
     var updateInterval: Decimal = 20
     var overrideHbA1cUnit: Bool = false
-    var displayLoops: Bool = false
 }
 
 extension Preferences {
@@ -111,11 +108,8 @@ extension Preferences {
         case enableSMB_high_bg
         case enableSMB_high_bg_target
         case threshold_setting
-        case high
-        case low
         case updateInterval
         case overrideHbA1cUnit
-        case displayLoops
     }
 }
 

+ 8 - 0
FreeAPS/Sources/Models/TIRforChart.swift

@@ -0,0 +1,8 @@
+
+import Foundation
+
+struct ShapeModel: Identifiable {
+    var type: String
+    var percent: Decimal
+    var id = UUID()
+}

+ 4 - 4
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -118,7 +118,7 @@ extension AddCarbs {
                             preset.protein = state.protein as NSDecimalNumber
                             preset.carbs = state.carbs as NSDecimalNumber
                             try? moc.save()
-                            state.selection = preset
+                            state.addNewPresetToWaitersNotepad(dish)
                             saved = false
                             isPromtPresented = false
                         }
@@ -175,19 +175,19 @@ extension AddCarbs {
                     )
                     Button {
                         if state.carbs != 0,
-                           (state.carbs - (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) as Decimal) > 0
+                           (state.carbs - (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
                         {
                             state.carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
                         } else { state.carbs = 0 }
 
                         if state.fat != 0,
-                           (state.fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) > 0
+                           (state.fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
                         {
                             state.fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
                         } else { state.fat = 0 }
 
                         if state.protein != 0,
-                           (state.protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) > 0
+                           (state.protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
                         {
                             state.protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
                         } else { state.protein = 0 }

+ 2 - 4
FreeAPS/Sources/Modules/AddTempTarget/AddTempTargetStateModel.swift

@@ -37,7 +37,7 @@ extension AddTempTarget {
             var lowTarget = low
 
             if viewPercantage {
-                lowTarget = computeTarget()
+                lowTarget = Decimal(round(Double(computeTarget())))
                 coredataContext.performAndWait {
                     let saveToCoreData = TempTargets(context: self.coredataContext)
                     saveToCoreData.id = UUID().uuidString
@@ -90,7 +90,6 @@ extension AddTempTarget {
                 let setHBT = TempTargetsSlider(context: self.coredataContext)
                 setHBT.enabled = false
                 setHBT.date = Date()
-
                 try? self.coredataContext.save()
             }
         }
@@ -102,10 +101,9 @@ extension AddTempTarget {
             var lowTarget = low
 
             if viewPercantage {
-                lowTarget = computeTarget()
+                lowTarget = Decimal(round(Double(computeTarget())))
                 saveSettings = true
             }
-
             var highTarget = lowTarget
 
             if units == .mmolL, !viewPercantage {

+ 28 - 26
FreeAPS/Sources/Modules/ConfigEditor/View/ConfigEditorRootView.swift

@@ -9,35 +9,37 @@ extension ConfigEditor {
         @State private var showShareSheet = false
 
         var body: some View {
-            TextEditor(text: $state.configText)
-                .keyboardType(.asciiCapable)
-                .font(.system(.subheadline, design: .monospaced))
-                .allowsTightening(true)
-                .autocapitalization(.none)
-                .disableAutocorrection(true)
-                .toolbar {
-                    ToolbarItemGroup(placement: .bottomBar) {
-                        Spacer()
-                        Button { showShareSheet = true }
-                        label: {
-                            Image(systemName: "square.and.arrow.up")
+            ZStack {
+                TextEditor(text: $state.configText)
+                    .keyboardType(.asciiCapable)
+                    .font(.system(.subheadline, design: .monospaced))
+                    .allowsTightening(true)
+                    .autocapitalization(.none)
+                    .disableAutocorrection(true)
+                    .toolbar {
+                        ToolbarItemGroup(placement: .bottomBar) {
+                            Spacer()
+                            Button { showShareSheet = true }
+                            label: {
+                                Image(systemName: "square.and.arrow.up")
+                            }
                         }
                     }
-                }
-                .navigationBarItems(
-                    trailing: Button("Save", action: state.save)
-                )
-                .sheet(isPresented: $showShareSheet) {
-                    ShareSheet(activityItems: [state.provider.urlFor(file: state.file)!])
-                }
-                .onAppear {
-                    configureView {
-                        state.file = file
+                    .navigationBarItems(
+                        trailing: Button("Save", action: state.save)
+                    )
+                    .sheet(isPresented: $showShareSheet) {
+                        ShareSheet(activityItems: [state.provider.urlFor(file: state.file)!])
                     }
-                }
-                .navigationTitle(file)
-                .navigationBarTitleDisplayMode(.inline)
-                .padding()
+                    .onAppear {
+                        configureView {
+                            state.file = file
+                        }
+                    }
+                    .navigationTitle(file)
+                    .navigationBarTitleDisplayMode(.inline)
+                    .padding()
+            }
         }
     }
 }

+ 0 - 41
FreeAPS/Sources/Modules/Home/DurationButton.swift

@@ -1,41 +0,0 @@
-import SwiftUI
-
-protocol DurationButton: CaseIterable {
-    var title: String { get }
-}
-
-extension DurationButton where Self: RawRepresentable, RawValue == String {
-    var title: String {
-        rawValue
-    }
-}
-
-enum durationState: String, DurationButton {
-    case day = "Past 24 Hours "
-    case week = "Past Week "
-    case month = "Past Month "
-    case total = "All Past Days of Data "
-}
-
-struct durationButton<T: DurationButton>: View {
-    let states: [T]
-    @State var currentIndex = 0
-    @Binding var selectedState: T
-
-    var body: some View {
-        Button {
-            currentIndex = currentIndex < states.count - 1 ? currentIndex + 1 : 0
-            selectedState = states[currentIndex]
-        } label: {
-            Text(NSLocalizedString(states[currentIndex].title, comment: "Duration displayed in statPanel"))
-                .font(.caption2)
-                .foregroundColor(.secondary)
-        }
-
-        .buttonBorderShape(.automatic)
-        .controlSize(.mini)
-        .buttonStyle(.bordered)
-        // .padding([.trailing], 15)
-        // .frame(maxWidth: .infinity, alignment: .trailing)
-    }
-}

+ 0 - 1
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -7,7 +7,6 @@ enum Home {
 
 protocol HomeProvider: Provider {
     var suggestion: Suggestion? { get }
-    var statistics: Statistics? { get }
     var enactedSuggestion: Suggestion? { get }
     func heartbeatNow()
     func filteredGlucose(hours: Int) -> [BloodGlucose]

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

@@ -14,14 +14,6 @@ extension Home {
             storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
         }
 
-        var statistics: Statistics? {
-            let stat = storage.retrieve(OpenAPS.Monitor.statistics, as: [Statistics].self)
-            if stat?.count ?? 0 != 0 {
-                return stat![0]
-            }
-            return nil
-        }
-
         var enactedSuggestion: Suggestion? {
             storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self)
         }

+ 9 - 22
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -13,7 +13,6 @@ extension Home {
 
         @Published var glucose: [BloodGlucose] = []
         @Published var suggestion: Suggestion?
-        @Published var statistics: Statistics?
         @Published var displayStatistics = false
         @Published var enactedSuggestion: Suggestion?
         @Published var recentGlucose: BloodGlucose?
@@ -46,15 +45,15 @@ extension Home {
         @Published var carbsRequired: Decimal?
         @Published var allowManualTemp = false
         @Published var units: GlucoseUnits = .mmolL
-        @Published var low: Decimal = 4
-        @Published var high: Decimal = 10
-        @Published var displayLoops = false
         @Published var pumpDisplayState: PumpDisplayState?
         @Published var alarm: GlucoseAlarm?
         @Published var animatedBackground = false
         @Published var manualTempBasal = false
         @Published var smooth = false
         @Published var maxValue: Decimal = 1.2
+        @Published var lowGlucoseLine: Decimal = 70
+        @Published var highGlucoseLine: Decimal = 145
+        @Published var overrideUnit = false
 
         override func subscribe() {
             setupGlucose()
@@ -67,14 +66,9 @@ extension Home {
             setupCarbs()
             setupBattery()
             setupReservoir()
-            setupStatistics()
 
             suggestion = provider.suggestion
-            statistics = provider.statistics
             displayStatistics = settingsManager.settings.displayStatistics
-            low = settingsManager.preferences.low
-            high = settingsManager.preferences.high
-            displayLoops = settingsManager.preferences.displayLoops
             enactedSuggestion = provider.enactedSuggestion
             units = settingsManager.settings.units
             allowManualTemp = !settingsManager.settings.closedLoop
@@ -87,6 +81,9 @@ extension Home {
             setupCurrentTempTarget()
             smooth = settingsManager.settings.smoothGlucose
             maxValue = settingsManager.preferences.autosensMax
+            lowGlucoseLine = settingsManager.settings.lowGlucose
+            highGlucoseLine = settingsManager.settings.highGlucose
+            overrideUnit = settingsManager.preferences.overrideHbA1cUnit
 
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(SuggestionObserver.self, observer: self)
@@ -321,13 +318,6 @@ extension Home {
             }
         }
 
-        private func setupStatistics() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.statistics = self.provider.statistics
-            }
-        }
-
         private func setupBattery() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
@@ -380,7 +370,6 @@ extension Home.StateModel:
 {
     func glucoseDidUpdate(_: [BloodGlucose]) {
         setupGlucose()
-        setupStatistics()
     }
 
     func suggestionDidUpdate(_ suggestion: Suggestion) {
@@ -393,15 +382,14 @@ extension Home.StateModel:
         allowManualTemp = !settings.closedLoop
         displayStatistics = settingsManager.settings.displayStatistics
         closedLoop = settingsManager.settings.closedLoop
-        low = settingsManager.preferences.low
-        high = settingsManager.preferences.high
-        displayLoops = settingsManager.preferences.displayLoops
         units = settingsManager.settings.units
         animatedBackground = settingsManager.settings.animatedBackground
         manualTempBasal = apsManager.isManualTempBasal
         smooth = settingsManager.settings.smoothGlucose
+        lowGlucoseLine = settingsManager.settings.lowGlucose
+        highGlucoseLine = settingsManager.settings.highGlucose
+        overrideUnit = settingsManager.preferences.overrideHbA1cUnit
         setupGlucose()
-        setupStatistics()
     }
 
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
@@ -429,7 +417,6 @@ extension Home.StateModel:
     func enactedSuggestionDidUpdate(_ suggestion: Suggestion) {
         enactedSuggestion = suggestion
         setStatusTitle()
-        setupStatistics()
     }
 
     func pumpBatteryDidChange(_: Battery) {

+ 20 - 21
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -19,26 +19,23 @@ typealias GlucoseYRange = (minValue: Int, minY: CGFloat, maxValue: Int, maxY: CG
 struct MainChartView: View {
     private enum Config {
         static let endID = "End"
-        static let screenHours = 6
+        static let screenHours = 8
         static let basalHeight: CGFloat = 80
         static let topYPadding: CGFloat = 20
-        static let bottomYPadding: CGFloat = 50
+        static let bottomYPadding: CGFloat = 80
         static let minAdditionalWidth: CGFloat = 150
-        static let maxGlucose = 250
-        static let minGlucose = 50
+        static let maxGlucose = 270
+        static let minGlucose = 45
         static let yLinesCount = 5
         static let glucoseScale: CGFloat = 2 // default 2
         static let bolusSize: CGFloat = 8
         static let bolusScale: CGFloat = 2.5
         static let carbsSize: CGFloat = 10
         static let carbsScale: CGFloat = 0.3
-        static let upperTarget: CGFloat = 180
-        static let lowerTarget: CGFloat = 70
     }
 
     @Binding var glucose: [BloodGlucose]
     @Binding var suggestion: Suggestion?
-    @Binding var statistcs: Statistics?
     @Binding var tempBasals: [PumpHistoryEvent]
     @Binding var boluses: [PumpHistoryEvent]
     @Binding var suspensions: [PumpHistoryEvent]
@@ -51,6 +48,8 @@ struct MainChartView: View {
     @Binding var timerDate: Date
     @Binding var units: GlucoseUnits
     @Binding var smooth: Bool
+    @Binding var highGlucoseLine: Decimal
+    @Binding var lowGlucoseLine: Decimal
 
     @State var didAppearTrigger = false
     @State private var glucoseDots: [CGRect] = []
@@ -66,7 +65,7 @@ struct MainChartView: View {
     @State private var carbsPath = Path()
     @State private var fpuDots: [DotInfo] = []
     @State private var fpuPath = Path()
-    @State private var glucoseYGange: GlucoseYRange = (0, 0, 0, 0)
+    @State private var glucoseYRange: GlucoseYRange = (0, 0, 0, 0)
     @State private var offset: CGFloat = 0
     @State private var cachedMaxBasalRate: Decimal?
 
@@ -170,27 +169,27 @@ struct MainChartView: View {
     private func yGridView(fullSize: CGSize) -> some View {
         ZStack {
             Path { path in
-                let range = glucoseYGange
+                let range = glucoseYRange
                 let step = (range.maxY - range.minY) / CGFloat(Config.yLinesCount)
                 for line in 0 ... Config.yLinesCount {
                     path.move(to: CGPoint(x: 0, y: range.minY + CGFloat(line) * step))
                     path.addLine(to: CGPoint(x: fullSize.width, y: range.minY + CGFloat(line) * step))
                 }
-            }.stroke(Color.secondary, lineWidth: 0.2)
+            }.stroke(Color.secondary, lineWidth: 0.15)
             // horizontal limits
-            let range = glucoseYGange
+            let range = glucoseYRange
             let topstep = (range.maxY - range.minY) / CGFloat(range.maxValue - range.minValue) *
-                (CGFloat(range.maxValue) - Config.upperTarget)
-            if CGFloat(range.maxValue) > Config.upperTarget {
+                (CGFloat(range.maxValue) - CGFloat(highGlucoseLine))
+            if CGFloat(range.maxValue) > CGFloat(highGlucoseLine) {
                 Path { path in
                     path.move(to: CGPoint(x: 0, y: range.minY + topstep))
                     path.addLine(to: CGPoint(x: fullSize.width, y: range.minY + topstep))
-                }.stroke(Color.loopYellow, lineWidth: 0.5)
+                }.stroke(Color.loopYellow, lineWidth: 0.5) // .StrokeStyle(lineWidth: 0.5, dash: [5])
             }
-            let yrange = glucoseYGange
+            let yrange = glucoseYRange
             let bottomstep = (yrange.maxY - yrange.minY) / CGFloat(yrange.maxValue - yrange.minValue) *
-                (CGFloat(yrange.maxValue) - Config.lowerTarget)
-            if CGFloat(yrange.minValue) < Config.lowerTarget {
+                (CGFloat(yrange.maxValue) - CGFloat(lowGlucoseLine))
+            if CGFloat(yrange.minValue) < CGFloat(lowGlucoseLine) {
                 Path { path in
                     path.move(to: CGPoint(x: 0, y: yrange.minY + bottomstep))
                     path.addLine(to: CGPoint(x: fullSize.width, y: yrange.minY + bottomstep))
@@ -201,7 +200,7 @@ struct MainChartView: View {
 
     private func glucoseLabelsView(fullSize: CGSize) -> some View {
         ForEach(0 ..< Config.yLinesCount + 1, id: \.self) { line -> AnyView in
-            let range = glucoseYGange
+            let range = glucoseYRange
             let yStep = (range.maxY - range.minY) / CGFloat(Config.yLinesCount)
             let valueStep = Double(range.maxValue - range.minValue) / Double(Config.yLinesCount)
             let value = round(Double(range.maxValue) - Double(line) * valueStep) *
@@ -274,7 +273,7 @@ struct MainChartView: View {
                     path.addLine(to: CGPoint(x: x, y: fullSize.height - 20))
                 }
             }
-            .stroke(Color.secondary, lineWidth: 0.2)
+            .stroke(Color.clear, lineWidth: 0.2)
 
             Path { path in // vertical timeline
                 let x = timeToXCoordinate(timerDate.timeIntervalSince1970, fullSize: fullSize)
@@ -488,7 +487,7 @@ extension MainChartView {
             let range = self.getGlucoseYRange(fullSize: fullSize)
 
             DispatchQueue.main.async {
-                glucoseYGange = range
+                glucoseYRange = range
                 glucoseDots = dots
             }
         }
@@ -504,7 +503,7 @@ extension MainChartView {
             let range = self.getGlucoseYRange(fullSize: fullSize)
 
             DispatchQueue.main.async {
-                glucoseYGange = range
+                glucoseYRange = range
                 unSmoothedGlucoseDots = dots
             }
         }

+ 6 - 5
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -5,6 +5,8 @@ struct CurrentGlucoseView: View {
     @Binding var delta: Int?
     @Binding var units: GlucoseUnits
     @Binding var alarm: GlucoseAlarm?
+    @Binding var lowGlucoseLine: Decimal
+    @Binding var highGlucoseLine: Decimal
 
     private var glucoseFormatter: NumberFormatter {
         let formatter = NumberFormatter()
@@ -110,13 +112,12 @@ struct CurrentGlucoseView: View {
         let whichGlucose = recentGlucose?.glucose ?? 0
 
         switch whichGlucose {
-        case 71 ... 145:
+        case Int(lowGlucoseLine ?? 70) + 1 ... Int(highGlucoseLine ?? 145) - 1:
             return .loopGreen
-        case 1 ... 55,
-             217...:
+        case 0 ... Int(lowGlucoseLine ?? 70),
+             201...:
             return .loopRed
-        case 56 ... 70,
-             146 ... 216:
+        case Int(highGlucoseLine ?? 145) ... 200:
             return .loopYellow
         default:
             return .primary

+ 16 - 264
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -10,7 +10,6 @@ extension Home {
 
         @StateObject var state = StateModel()
         @State var isStatusPopupPresented = false
-        @State var selectedState: durationState
 
         // Average/Median/Readings and CV/SD titles and values switches when you tap them
         @State var averageOrMedianTitle = NSLocalizedString("Average", comment: "")
@@ -119,7 +118,9 @@ extension Home {
                 recentGlucose: $state.recentGlucose,
                 delta: $state.glucoseDelta,
                 units: $state.units,
-                alarm: $state.alarm
+                alarm: $state.alarm,
+                lowGlucoseLine: $state.lowGlucoseLine,
+                highGlucoseLine: $state.highGlucoseLine
             )
             .onTapGesture {
                 if state.alarm == nil {
@@ -218,7 +219,6 @@ extension Home {
             let percentString = "\((fetchedPercent.first?.percentage ?? 100).formatted(.number)) %"
             let durationString = (fetchedPercent.first?.indefinite ?? false) ?
                 "" : ", " + (tirFormatter.string(from: (fetchedPercent.first?.duration ?? 0) as NSNumber) ?? "") + " min"
-
             return percentString + durationString
         }
 
@@ -264,264 +264,6 @@ extension Home {
             .frame(maxWidth: .infinity, maxHeight: 30)
         }
 
-        @ViewBuilder private func statPanel() -> some View {
-            if state.displayStatistics {
-                VStack(spacing: 8) {
-                    durationButton(states: durationState.allCases, selectedState: $selectedState)
-
-                    switch selectedState {
-                    case .day:
-
-                        let hba1c_all = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
-                        let average_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Average.day ?? 0) as NSNumber) ?? ""
-                        let median_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Median.day ?? 0) as NSNumber) ?? ""
-                        let tir_low = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypos.day ?? 0) as NSNumber) ?? ""
-                        let tir_high = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypers.day ?? 0) as NSNumber) ?? ""
-                        let tir_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.TIR.day ?? 0) as NSNumber) ?? ""
-                        let hba1c_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.day ?? 0) as NSNumber) ?? ""
-                        let sd_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.SD.day ?? 0) as NSNumber) ?? ""
-                        let cv_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.CV.day ?? 0) as NSNumber) ?? ""
-
-                        averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
-
-                    case .week:
-                        let hba1c_all = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
-                        let average_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Average.week ?? 0) as NSNumber) ?? ""
-                        let median_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Median.week ?? 0) as NSNumber) ?? ""
-                        let tir_low = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypos.week ?? 0) as NSNumber) ?? ""
-                        let tir_high = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypers.week ?? 0) as NSNumber) ?? ""
-                        let tir_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.TIR.week ?? 0) as NSNumber) ?? ""
-                        let hba1c_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.week ?? 0) as NSNumber) ?? ""
-                        let sd_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.SD.week ?? 0) as NSNumber) ?? ""
-                        let cv_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.CV.week ?? 0) as NSNumber) ?? ""
-
-                        averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
-
-                    case .month:
-                        let hba1c_all = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
-                        let average_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Average.month ?? 0) as NSNumber) ?? ""
-                        let median_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Median.month ?? 0) as NSNumber) ?? ""
-                        let tir_low = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypos.month ?? 0) as NSNumber) ?? ""
-                        let tir_high = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypers.month ?? 0) as NSNumber) ?? ""
-                        let tir_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.TIR.month ?? 0) as NSNumber) ?? ""
-                        let hba1c_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.month ?? 0) as NSNumber) ?? ""
-                        let sd_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.SD.month ?? 0) as NSNumber) ?? ""
-                        let cv_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.CV.month ?? 0) as NSNumber) ?? ""
-
-                        averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
-
-                    case .total:
-                        let hba1c_all = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
-                        let average_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Average.total ?? 0) as NSNumber) ?? ""
-                        let median_ = targetFormatter
-                            .string(from: (state.statistics?.Statistics.Glucose.Median.total ?? 0) as NSNumber) ?? ""
-                        let tir_low = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypos.total ?? 0) as NSNumber) ?? ""
-                        let tir_high = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.Hypers.total ?? 0) as NSNumber) ??
-                            ""
-                        let tir_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Distribution.TIR.total ?? 0) as NSNumber) ?? ""
-                        let hba1c_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.HbA1c.total ?? 0) as NSNumber) ?? ""
-                        let sd_ = numberFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.SD.total ?? 0) as NSNumber) ?? ""
-                        let cv_ = tirFormatter
-                            .string(from: (state.statistics?.Statistics.Variance.CV.total ?? 0) as NSNumber) ?? ""
-
-                        averageTIRhca1c(hba1c_all, average_, median_, tir_low, tir_high, tir_, hba1c_, sd_, cv_)
-                    }
-                }
-                .frame(maxWidth: .infinity)
-                .padding([.bottom], 20)
-            }
-        }
-
-        @ViewBuilder private func averageTIRhca1c(
-            _ hba1c_all: String,
-            _ average_: String,
-            _ median_: String,
-            _ tir_low: String,
-            _ tir_high: String,
-            _ tir_: String,
-            _ hba1c_: String,
-            _ sd_: String,
-            _ cv_: String
-        ) -> some View {
-            HStack {
-                Group {
-                    if selectedState != .total {
-                        HStack {
-                            Text("HbA1c").font(.footnote).foregroundColor(.secondary)
-                            Text(hba1c_).font(.footnote)
-                        }
-                    } else {
-                        HStack {
-                            Text(
-                                "\(NSLocalizedString("HbA1c", comment: "")) (\(targetFormatter.string(from: (state.statistics?.GlucoseStorage_Days ?? 0) as NSNumber) ?? "") \(NSLocalizedString("days", comment: "")))"
-                            )
-                            .font(.footnote).foregroundColor(.secondary)
-                            Text(hba1c_all).font(.footnote)
-                        }
-                    }
-                    // Average as default. Changes to Median when clicking.
-                    let textAverageTitle = NSLocalizedString("Average", comment: "")
-                    let textMedianTitle = NSLocalizedString("Median", comment: "")
-                    let cgmReadingsTitle = NSLocalizedString("Readings", comment: "CGM readings in statPanel")
-
-                    HStack {
-                        Text(averageOrMedianTitle).font(.footnote).foregroundColor(.secondary)
-                        if averageOrMedianTitle == textAverageTitle {
-                            Text(averageOrmedian == "" ? average_ : average_).font(.footnote)
-                        } else if averageOrMedianTitle == textMedianTitle {
-                            Text(averageOrmedian == "" ? median_ : median_).font(.footnote)
-                        } else if averageOrMedianTitle == cgmReadingsTitle {
-                            Text(
-                                averageOrmedian != "0" ? tirFormatter
-                                    .string(from: (state.statistics?.Statistics.LoopCycles.readings ?? 0) as NSNumber) ?? "" : ""
-                            )
-                            .font(.footnote)
-                        }
-                    }.onTapGesture {
-                        if averageOrMedianTitle == textAverageTitle {
-                            averageOrMedianTitle = textMedianTitle
-                            averageOrmedian = median_
-                        } else if averageOrMedianTitle == textMedianTitle {
-                            averageOrMedianTitle = cgmReadingsTitle
-                            averageOrmedian = tirFormatter
-                                .string(from: (state.statistics?.Statistics.LoopCycles.readings ?? 0) as NSNumber) ?? ""
-                        } else if averageOrMedianTitle == cgmReadingsTitle {
-                            averageOrMedianTitle = textAverageTitle
-                            averageOrmedian = average_
-                        }
-                    }
-                    .frame(minWidth: 110)
-                    // CV as default. Changes to SD when clicking
-                    let text_CV_Title = NSLocalizedString("CV", comment: "")
-                    let text_SD_Title = NSLocalizedString("SD", comment: "")
-
-                    HStack {
-                        Text(CV_or_SD_Title).font(.footnote).foregroundColor(.secondary)
-                        if CV_or_SD_Title == text_CV_Title {
-                            Text(CVorSD == "" ? cv_ : cv_).font(.footnote)
-                        } else {
-                            Text(CVorSD == "" ? sd_ : sd_).font(.footnote)
-                        }
-                    }.onTapGesture {
-                        if CV_or_SD_Title == text_CV_Title {
-                            CV_or_SD_Title = text_SD_Title
-                            CVorSD = sd_
-                        } else {
-                            CV_or_SD_Title = text_CV_Title
-                            CVorSD = cv_
-                        }
-                    }
-                }
-            }
-            HStack {
-                Group {
-                    HStack {
-                        Text(
-                            NSLocalizedString("Low", comment: " ")
-                        )
-                        .font(.footnote)
-                        .foregroundColor(.secondary)
-
-                        Text(tir_low + " %").font(.footnote).foregroundColor(.loopRed)
-                    }
-
-                    HStack {
-                        Text("Normal").font(.footnote).foregroundColor(.secondary)
-                        Text(tir_ + " %").font(.footnote).foregroundColor(.loopGreen)
-                    }
-
-                    HStack {
-                        Text(
-                            NSLocalizedString("High", comment: " ")
-                        )
-                        .font(.footnote).foregroundColor(.secondary)
-
-                        Text(tir_high + " %").font(.footnote).foregroundColor(.loopYellow)
-                    }
-                }
-            }
-
-            if state.settingsManager.preferences.displayLoops {
-                HStack {
-                    Group {
-                        let loopTitle = NSLocalizedString("Loops", comment: "Nr of Loops in statPanel")
-                        let errorTitle = NSLocalizedString("Errors", comment: "Loop Errors in statPanel")
-
-                        HStack {
-                            Text(loopStatTitle).font(.footnote).foregroundColor(.secondary)
-                            Text(
-                                loopStatTitle == loopTitle ? tirFormatter
-                                    .string(from: (state.statistics?.Statistics.LoopCycles.loops ?? 0) as NSNumber) ?? "" :
-                                    tirFormatter
-                                    .string(from: (state.statistics?.Statistics.LoopCycles.errors ?? 0) as NSNumber) ?? ""
-                            ).font(.footnote)
-                        }.onTapGesture {
-                            if loopStatTitle == loopTitle {
-                                loopStatTitle = errorTitle
-                            } else if loopStatTitle == errorTitle {
-                                loopStatTitle = loopTitle
-                            }
-                        }
-
-                        HStack {
-                            Text("Interval").font(.footnote)
-                                .foregroundColor(.secondary)
-                            Text(
-                                targetFormatter
-                                    .string(from: (state.statistics?.Statistics.LoopCycles.avg_interval ?? 0) as NSNumber) ??
-                                    ""
-                            ).font(.footnote)
-                        }
-
-                        HStack {
-                            Text("Duration").font(.footnote)
-                                .foregroundColor(.secondary)
-                            Text(
-                                numberFormatter
-                                    .string(
-                                        from: (state.statistics?.Statistics.LoopCycles.median_duration ?? 0) as NSNumber
-                                    ) ?? ""
-                            ).font(.footnote)
-                        }
-                    }
-                }
-            }
-        }
-
         var legendPanel: some View {
             ZStack {
                 HStack(alignment: .center) {
@@ -580,7 +322,6 @@ extension Home {
                 MainChartView(
                     glucose: $state.glucose,
                     suggestion: $state.suggestion,
-                    statistcs: $state.statistics,
                     tempBasals: $state.tempBasals,
                     boluses: $state.boluses,
                     suspensions: $state.suspensions,
@@ -592,7 +333,9 @@ extension Home {
                     carbs: $state.carbs,
                     timerDate: $state.timerDate,
                     units: $state.units,
-                    smooth: $state.smooth
+                    smooth: $state.smooth,
+                    highGlucoseLine: $state.highGlucoseLine,
+                    lowGlucoseLine: $state.lowGlucoseLine
                 )
             }
             .padding(.bottom)
@@ -652,6 +395,16 @@ extension Home {
                         }.foregroundColor(.insulin)
                         Spacer()
                     }
+                    Button { state.showModal(for: .statistics)
+                    }
+                    label: {
+                        Image(systemName: "chart.xyaxis.line")
+                            .renderingMode(.template)
+                            .resizable()
+                            .frame(width: 24, height: 24)
+                            .padding(8)
+                    }.foregroundColor(.purple)
+                    Spacer()
                     Button { state.showModal(for: .settings) }
                     label: {
                         Image("settings1")
@@ -673,7 +426,6 @@ extension Home {
                     infoPanel
                     mainChart
                     legendPanel
-                    statPanel()
                     bottomPanel(geo)
                 }
                 .edgesIgnoringSafeArea(.vertical)

+ 1 - 0
FreeAPS/Sources/Modules/Main/MainStateModel.swift

@@ -107,6 +107,7 @@ extension Main {
     }
 }
 
+@available(iOS 16.0, *)
 extension Main.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
         // close the window

+ 0 - 51
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift

@@ -26,57 +26,6 @@ extension PreferencesEditor {
             let statFields = [
                 Field(
                     displayName: NSLocalizedString(
-                        "Low Glucose Limit",
-                        comment: "Display As Low Glucose Percantage Under This Value"
-                    ) + " (\(settingsManager.settings.units.rawValue))",
-
-                    type: .decimal(keypath: \.low),
-                    infoText: NSLocalizedString(
-                        "Blood Glucoses Under This Value Will Added To And Displayed as Low Glucose Percantage",
-                        comment: "Description for Low Glucose Limit"
-                    ),
-                    settable: self
-                ),
-                Field(
-                    displayName: NSLocalizedString(
-                        "High Glucose Limit",
-                        comment: "Limit For High Glucose in Statistics View"
-                    ) + " (\(settingsManager.settings.units.rawValue))",
-
-                    type: .decimal(keypath: \.high),
-                    infoText: NSLocalizedString(
-                        "Blood Glucoses Over This Value Will Added To And Displaved as High Glucose Percantage",
-                        comment: "High Glucose Limit"
-                    ),
-                    settable: self
-                ),
-                Field(
-                    displayName: NSLocalizedString(
-                        "Update every number of minutes:",
-                        comment: "How often to update the statistics"
-                    ),
-
-                    type: .decimal(keypath: \.updateInterval),
-                    infoText: NSLocalizedString(
-                        "Default is 20 minutes. How often to update and save the statistics.json and to upload last array, when enabled, to Nightscout.",
-                        comment: "Description for update interval for statistics"
-                    ),
-                    settable: self
-                ),
-                Field(
-                    displayName: NSLocalizedString(
-                        "Display Loop Cycle statistics",
-                        comment: "Display Display Loop Cycle statistics in statPanel"
-                    ),
-                    type: .boolean(keypath: \.displayLoops),
-                    infoText: NSLocalizedString(
-                        "Displays Loop statistics in the statPanel in Home View",
-                        comment: "Description for Display Loop statistics"
-                    ),
-                    settable: self
-                ),
-                Field(
-                    displayName: NSLocalizedString(
                         "Override HbA1c unit",
                         comment: "Display %"
                     ),

+ 1 - 1
FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift

@@ -37,7 +37,7 @@ extension PreferencesEditor {
 
                     Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)
 
-                    Toggle("Display Statistics", isOn: $state.displayStatistics)
+                    Toggle("Allow Upload of Statistics to NS", isOn: $state.displayStatistics)
                 }
 
                 ForEach(state.sections.indexed(), id: \.1.id) { sectionIndex, section in

+ 5 - 0
FreeAPS/Sources/Modules/Stat/StatDataFlow.swift

@@ -0,0 +1,5 @@
+enum Stat {
+    enum Config {}
+}
+
+protocol StatProvider: Provider {}

+ 3 - 0
FreeAPS/Sources/Modules/Stat/StatProvider.swift

@@ -0,0 +1,3 @@
+extension Stat {
+    final class Provider: BaseProvider, StatProvider {}
+}

+ 21 - 0
FreeAPS/Sources/Modules/Stat/StatStateModel.swift

@@ -0,0 +1,21 @@
+import Foundation
+import SwiftUI
+import Swinject
+
+extension Stat {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var settings: SettingsManager!
+        @Published var highLimit: Decimal?
+        @Published var lowLimit: Decimal?
+        @Published var overrideUnit: Bool?
+
+        private(set) var units: GlucoseUnits = .mmolL
+
+        override func subscribe() {
+            highLimit = settingsManager.settings.highGlucose
+            lowLimit = settingsManager.settings.lowGlucose
+            units = settingsManager.settings.units
+            overrideUnit = settingsManager.preferences.overrideHbA1cUnit
+        }
+    }
+}

+ 767 - 0
FreeAPS/Sources/Modules/Stat/View/StatRootView.swift

@@ -0,0 +1,767 @@
+import Charts
+import CoreData
+import SwiftDate
+import SwiftUI
+import Swinject
+
+extension Stat {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        // @Environment(\.managedObjectContext) var moc
+        @FetchRequest(
+            entity: Readings.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
+                format: "date >= %@", Calendar.current.startOfDay(for: Date()) as NSDate
+            )
+        ) var fetchedGlucoseDay: FetchedResults<Readings>
+
+        @FetchRequest(
+            entity: Readings.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
+            predicate: NSPredicate(format: "date > %@", Date().addingTimeInterval(-24.hours.timeInterval) as NSDate)
+        ) var fetchedGlucoseTwentyFourHours: FetchedResults<Readings>
+
+        @FetchRequest(
+            entity: Readings.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
+            predicate: NSPredicate(format: "date > %@", Date().addingTimeInterval(-7.days.timeInterval) as NSDate)
+        ) var fetchedGlucoseWeek: FetchedResults<Readings>
+
+        @FetchRequest(
+            entity: Readings.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
+                format: "date > %@",
+                Date().addingTimeInterval(-30.days.timeInterval) as NSDate
+            )
+        ) var fetchedGlucoseMonth: FetchedResults<Readings>
+
+        @FetchRequest(
+            entity: Readings.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
+                format: "date > %@",
+                Date().addingTimeInterval(-90.days.timeInterval) as NSDate
+            )
+        ) var fetchedGlucose: FetchedResults<Readings>
+
+        @FetchRequest(
+            entity: TDD.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "timestamp", ascending: false)]
+        ) var fetchedTDD: FetchedResults<TDD>
+
+        @FetchRequest(
+            entity: LoopStatRecord.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)], predicate: NSPredicate(
+                format: "start > %@",
+                Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
+            )
+        ) var fetchedLoopStats: FetchedResults<LoopStatRecord>
+
+        @FetchRequest(
+            entity: InsulinDistribution.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
+        ) var fetchedInsulin: FetchedResults<InsulinDistribution>
+
+        enum Duration: String, CaseIterable, Identifiable {
+            case Today
+            case Day
+            case Week
+            case Month
+            case Total
+            var id: Self { self }
+        }
+
+        @State private var selectedDuration: Duration = .Today
+        @State var paddingAmount: CGFloat? = 10
+        @State var headline: Color = .secondary
+        @State var days: Double = 0
+        @State var pointSize: CGFloat = 3
+        @State var conversionFactor = 0.0555
+
+        @ViewBuilder func stats() -> some View {
+            bloodGlucose
+            Divider()
+            tirChart
+            Divider()
+            hba1c
+            Divider()
+            loops
+        }
+
+        @ViewBuilder func chart() -> some View {
+            Text("Statistics").font(.largeTitle).bold().padding(.top, 25)
+            switch selectedDuration {
+            case .Today:
+                glucoseChart
+            case .Day:
+                glucoseChartTwentyFourHours
+            case .Week:
+                glucoseChartWeek
+            case .Month:
+                glucoseChartMonth
+            case .Total:
+                glucoseChart90
+            }
+        }
+
+        var body: some View {
+            ZStack {
+                VStack(alignment: .center, spacing: 8) {
+                    chart().padding(.horizontal, 10)
+                    Divider()
+                    stats()
+                    Spacer()
+                    Picker("Duration", selection: $selectedDuration) {
+                        ForEach(Duration.allCases) { duration in
+                            Text(duration.rawValue).tag(Optional(duration))
+                        }
+                    }.pickerStyle(.segmented)
+                }
+            }.onAppear(perform: configureView)
+        }
+
+        var loops: some View {
+            VStack {
+                let loops_ = loopStats(fetchedLoopStats)
+                HStack {
+                    ForEach(0 ..< loops_.count, id: \.self) { index in
+                        VStack {
+                            Text(loops_[index].string).font(.subheadline).foregroundColor(.secondary)
+                            Text(
+                                index == 0 ? loops_[index].double.formatted() : (
+                                    index == 2 ? loops_[index].double
+                                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(2))) :
+                                        loops_[index]
+                                        .double
+                                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                                )
+                            )
+                        }.padding(.horizontal, 6)
+                    }
+                }
+            }
+        }
+
+        var hba1c: some View {
+            let useUnit: GlucoseUnits = (state.units == .mmolL && (state.overrideUnit ?? false)) ? .mgdL :
+                (state.units == .mgdL && (state.overrideUnit ?? false) || state.units == .mmolL) ? .mmolL : .mgdL
+            return HStack {
+                let hba1cs = glucoseStats(fetchedGlucose)
+                let hba1cString = (
+                    useUnit == .mmolL ? hba1cs.ifcc
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : hba1cs.ngsp
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                ) + " %"
+
+                VStack {
+                    Text("HbA1C").font(.subheadline).foregroundColor(headline)
+                    HStack {
+                        VStack {
+                            Text(hba1cString)
+                        }
+                    }
+                }.padding([.horizontal], 15)
+                VStack {
+                    Text("SD").font(.subheadline).foregroundColor(.secondary)
+                    HStack {
+                        VStack {
+                            Text(
+                                hba1cs.sd
+                                    .formatted(
+                                        .number.grouping(.never).rounded()
+                                            .precision(.fractionLength(state.units == .mmolL ? 1 : 0))
+                                    )
+                            )
+                        }
+                    }
+                }.padding([.horizontal], 15)
+                VStack {
+                    Text("CV").font(.subheadline).foregroundColor(.secondary)
+                    HStack {
+                        VStack {
+                            Text(
+                                hba1cs.cv.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
+                            )
+                        }
+                    }
+                }.padding([.horizontal], 15)
+                if selectedDuration == .Total {
+                    VStack {
+                        Text("Days").font(.subheadline).foregroundColor(.secondary)
+                        HStack {
+                            VStack {
+                                Text(numberOfDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))))
+                            }
+                        }
+                    }.padding([.horizontal], 15)
+                }
+            }
+        }
+
+        var bloodGlucose: some View {
+            VStack {
+                HStack {
+                    let bgs = glucoseStats(fetchedGlucose)
+                    VStack {
+                        HStack {
+                            Text("Average").font(.subheadline).foregroundColor(headline)
+                        }
+                        HStack {
+                            VStack {
+                                Text(
+                                    bgs.average
+                                        .formatted(
+                                            .number.grouping(.never).rounded()
+                                                .precision(.fractionLength(state.units == .mmolL ? 1 : 0))
+                                        )
+                                )
+                            }
+                        }
+                    }
+                    VStack {
+                        HStack {
+                            Text("Median").font(.subheadline).foregroundColor(.secondary)
+                        }
+                        HStack {
+                            VStack {
+                                Text(
+                                    bgs.median
+                                        .formatted(
+                                            .number.grouping(.never).rounded()
+                                                .precision(.fractionLength(state.units == .mmolL ? 1 : 0))
+                                        )
+                                )
+                            }
+                        }
+                    }
+                    VStack {
+                        HStack {
+                            Text(selectedDuration == .Today ? "Readings today" : "Readings / 24h").font(.subheadline)
+                                .foregroundColor(.secondary)
+                        }
+                        HStack {
+                            VStack {
+                                Text(
+                                    bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        var numberOfDays: Double {
+            let endIndex = fetchedGlucose.count - 1
+            var days = 0.0
+
+            if endIndex > 0 {
+                let firstElementTime = fetchedGlucose.first?.date ?? Date()
+                let lastElementTime = fetchedGlucose[endIndex].date ?? Date()
+                days = (firstElementTime - lastElementTime).timeInterval / 8.64E4
+            }
+            return days
+        }
+
+        var tirChart: some View {
+            let array = selectedDuration == .Today ? fetchedGlucoseDay : selectedDuration == .Day ?
+                fetchedGlucoseTwentyFourHours :
+                selectedDuration == .Week ? fetchedGlucoseWeek : selectedDuration == .Month ? fetchedGlucoseMonth :
+                selectedDuration ==
+                .Total ? fetchedGlucose : fetchedGlucoseDay
+            let fetched = tir(array)
+            let data: [ShapeModel] = [
+                .init(type: "Low", percent: fetched[0].decimal),
+                .init(type: "In Range", percent: fetched[1].decimal),
+                .init(type: "High", percent: fetched[2].decimal)
+            ]
+
+            return VStack(alignment: .center) {
+                Chart(data) { shape in
+                    BarMark(
+                        x: .value("Shape", shape.type),
+                        y: .value("Percentage", shape.percent)
+                    )
+                    .foregroundStyle(by: .value("Group", shape.type))
+                    .annotation(position: shape.percent < 5 ? .top : .overlay, alignment: .center) {
+                        Text(shape.percent == 0 ? "" : "\(shape.percent, format: .number.precision(.fractionLength(0))) %")
+                        // .foregroundColor(.white)
+                    }
+                }
+                .chartYAxis(.hidden)
+                .chartLegend(.hidden)
+                .chartForegroundStyleScale(["Low": .red, "In Range": .green, "High": .orange])
+            }
+        }
+
+        var glucoseChart: some View {
+            let count = fetchedGlucoseDay.count
+            let lowLimit = (state.lowLimit ?? 70) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            let highLimit = (state.highLimit ?? 145) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            return Chart {
+                ForEach(fetchedGlucoseDay.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.orange)
+                    .symbolSize(count < 20 ? 30 : 12)
+                }
+                ForEach(
+                    fetchedGlucoseDay
+                        .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
+                    id: \.date
+                ) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.green)
+                    .symbolSize(count < 20 ? 30 : 12)
+                }
+                ForEach(fetchedGlucoseDay.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.red)
+                    .symbolSize(count < 20 ? 30 : 12)
+                }
+            }
+            .chartYScale(domain: [0, 17])
+            .chartYAxis {
+                AxisMarks(
+                    values: [
+                        0,
+                        lowLimit,
+                        highLimit,
+                        15
+                    ]
+                )
+            }
+        }
+
+        var glucoseChartTwentyFourHours: some View {
+            let count = fetchedGlucoseTwentyFourHours.count
+            let lowLimit = (state.lowLimit ?? 70) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            let highLimit = (state.highLimit ?? 145) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            return Chart {
+                ForEach(fetchedGlucoseTwentyFourHours.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.orange)
+                    .symbolSize(count < 20 ? 20 : 10)
+                }
+                ForEach(
+                    fetchedGlucoseTwentyFourHours
+                        .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
+                    id: \.date
+                ) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.green)
+                    .symbolSize(count < 20 ? 20 : 10)
+                }
+                ForEach(fetchedGlucoseTwentyFourHours.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.red)
+                    .symbolSize(count < 20 ? 20 : 10)
+                }
+                RuleMark(
+                    y: .value("Target", 100 * (state.units == .mmolL ? conversionFactor : 1))
+                )
+                .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [10]))
+            }
+            .chartYScale(domain: [0, 17])
+            .chartYAxis {
+                AxisMarks(
+                    values: [
+                        0,
+                        lowLimit,
+                        highLimit,
+                        15
+                    ]
+                )
+            }
+        }
+
+        var glucoseChartWeek: some View {
+            let lowLimit = (state.lowLimit ?? 70) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            let highLimit = (state.highLimit ?? 145) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            return Chart {
+                ForEach(fetchedGlucoseWeek.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.orange)
+                    .symbolSize(5)
+                }
+                ForEach(
+                    fetchedGlucoseWeek
+                        .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
+                    id: \.date
+                ) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.green)
+                    .symbolSize(5)
+                }
+                ForEach(fetchedGlucoseWeek.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.red)
+                    .symbolSize(5)
+                }
+                RuleMark(
+                    y: .value("Target", 100 * (state.units == .mmolL ? conversionFactor : 1))
+                )
+                .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [10]))
+            }
+            .chartYScale(domain: [0, 17])
+            .chartYAxis {
+                AxisMarks(
+                    values: [
+                        0,
+                        lowLimit,
+                        highLimit,
+                        15
+                    ]
+                )
+            }
+        }
+
+        var glucoseChartMonth: some View {
+            let lowLimit = (state.lowLimit ?? 70) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            let highLimit = (state.highLimit ?? 145) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            return Chart {
+                ForEach(fetchedGlucoseMonth.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.orange)
+                    .symbolSize(2)
+                }
+                ForEach(
+                    fetchedGlucoseMonth
+                        .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
+                    id: \.date
+                ) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.green)
+                    .symbolSize(2)
+                }
+                ForEach(fetchedGlucoseMonth.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.red)
+                    .symbolSize(2)
+                }
+                RuleMark(
+                    y: .value("Target", 100 * (state.units == .mmolL ? conversionFactor : 1))
+                )
+                .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [10]))
+            }
+            .chartYScale(domain: [0, 17])
+            .chartYAxis {
+                AxisMarks(
+                    values: [
+                        0,
+                        lowLimit,
+                        highLimit,
+                        15
+                    ]
+                )
+            }
+        }
+
+        var glucoseChart90: some View {
+            let lowLimit = (state.lowLimit ?? 70) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            let highLimit = (state.highLimit ?? 145) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
+            return Chart {
+                ForEach(fetchedGlucose.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.orange)
+                    .symbolSize(2)
+                }
+                ForEach(
+                    fetchedGlucose
+                        .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
+                    id: \.date
+                ) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.green)
+                    .symbolSize(2)
+                }
+                ForEach(fetchedGlucose.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
+                    PointMark(
+                        x: .value("Date", item.date ?? Date()),
+                        y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
+                    )
+                    .foregroundStyle(.red)
+                    .symbolSize(2)
+                }
+                RuleMark(
+                    y: .value("Target", 100 * (state.units == .mmolL ? conversionFactor : 1))
+                )
+                .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [10]))
+            }
+            .chartYScale(domain: [0, 17])
+            .chartYAxis {
+                AxisMarks(
+                    values: [
+                        0,
+                        lowLimit,
+                        highLimit,
+                        15
+                    ]
+                )
+            }
+        }
+
+        private func loopStats(_ loops: FetchedResults<LoopStatRecord>) -> [(double: Double, string: String)] {
+            guard (loops.first?.start) != nil else { return [] }
+
+            var i = 0.0
+            var minimumInt = 999.0
+            var maximumInt = 0.0
+            var timeIntervalLoops = 0.0
+            var previousTimeLoop = loops.first?.end ?? Date()
+            var timeIntervalLoopArray: [Double] = []
+
+            let durationArray = loops.compactMap({ each in each.duration })
+            let durationArrayCount = durationArray.count
+            var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
+
+            let medianDuration = medianCalculationDouble(array: durationArray)
+            let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
+            let errorNR = durationArrayCount - successsNR
+            let successRate: Double? = (Double(successsNR) / Double(successsNR + errorNR)) * 100
+
+            for each in loops {
+                if let loopEnd = each.end {
+                    i += 1
+                    timeIntervalLoops = (previousTimeLoop - (each.start ?? previousTimeLoop)).timeInterval / 60
+
+                    if timeIntervalLoops > 0.0, i != 1 {
+                        timeIntervalLoopArray.append(timeIntervalLoops)
+                    }
+                    if timeIntervalLoops > maximumInt {
+                        maximumInt = timeIntervalLoops
+                    }
+                    if timeIntervalLoops < minimumInt, i != 1 {
+                        minimumInt = timeIntervalLoops
+                    }
+                    previousTimeLoop = loopEnd
+                }
+            }
+
+            // Average Loop Interval in minutes
+            let timeOfFirstIndex = loops.first?.start ?? Date()
+            let lastIndexWithTimestamp = loops.count - 1
+            let timeOfLastIndex = loops[lastIndexWithTimestamp].end ?? Date()
+            let averageInterval = (timeOfFirstIndex - timeOfLastIndex).timeInterval / 60 / Double(errorNR + successsNR)
+
+            if minimumInt == 999.0 {
+                minimumInt = 0.0
+            }
+
+            var array: [(double: Double, string: String)] = []
+
+            array.append((double: Double(successsNR + errorNR), string: "Loops"))
+            array.append((double: averageInterval, string: "Interval"))
+            array.append((double: medianDuration, string: "Duration"))
+            array.append((double: successRate ?? 100, string: "%"))
+
+            return array
+        }
+
+        private func medianCalculation(array: [Int]) -> Double {
+            guard !array.isEmpty else {
+                return 0
+            }
+            let sorted = array.sorted()
+            let length = array.count
+
+            if length % 2 == 0 {
+                return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
+            }
+            return Double(sorted[length / 2])
+        }
+
+        private func medianCalculationDouble(array: [Double]) -> Double {
+            guard !array.isEmpty else {
+                return 0
+            }
+            let sorted = array.sorted()
+            let length = array.count
+
+            if length % 2 == 0 {
+                return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
+            }
+            return sorted[length / 2]
+        }
+
+        private func glucoseStats(_ glucose_90: FetchedResults<Readings>)
+            -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
+        {
+            var numberOfDays: Double = 0
+            let endIndex = glucose_90.count - 1
+
+            if endIndex > 0 {
+                let firstElementTime = glucose_90[0].date ?? Date()
+                let lastElementTime = glucose_90[endIndex].date ?? Date()
+                numberOfDays = (firstElementTime - lastElementTime).timeInterval / 8.64E4
+            }
+            var duration = 1
+            var denominator: Double = 1
+
+            switch selectedDuration {
+            case .Today:
+                let minutesSinceMidnight = Calendar.current.component(.hour, from: Date()) * 60 + Calendar.current
+                    .component(.minute, from: Date())
+                duration = minutesSinceMidnight
+                denominator = 1
+            case .Day:
+                duration = 1 * 1440
+                denominator = 1
+            case .Week:
+                duration = 7 * 1440
+                if numberOfDays > 7 { denominator = 7 } else { denominator = numberOfDays }
+            case .Month:
+                duration = 30 * 1440
+                if numberOfDays > 30 { denominator = 30 } else { denominator = numberOfDays }
+            case .Total:
+                duration = 90 * 1440
+                if numberOfDays >= 90 { denominator = 90 } else { denominator = numberOfDays }
+            }
+
+            let timeAgo = Date().addingTimeInterval(-duration.minutes.timeInterval)
+            let glucose = glucose_90.filter({ ($0.date ?? Date()) >= timeAgo })
+
+            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+            let sumReadings = justGlucoseArray.reduce(0, +)
+            let countReadings = justGlucoseArray.count
+
+            let glucoseAverage = Double(sumReadings) / Double(countReadings)
+            let medianGlucose = medianCalculation(array: justGlucoseArray)
+
+            var NGSPa1CStatisticValue = 0.0
+            var IFCCa1CStatisticValue = 0.0
+
+            if numberOfDays > 0 {
+                NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
+                IFCCa1CStatisticValue = 10.929 *
+                    (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol)  A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
+            }
+            var sumOfSquares = 0.0
+
+            for array in justGlucoseArray {
+                sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
+            }
+            var sd = 0.0
+            var cv = 0.0
+
+            // Avoid division by zero
+            if glucoseAverage > 0 {
+                sd = sqrt(sumOfSquares / Double(countReadings))
+                cv = sd / Double(glucoseAverage) * 100
+            }
+
+            var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
+            output = (
+                ifcc: IFCCa1CStatisticValue,
+                ngsp: NGSPa1CStatisticValue,
+                average: glucoseAverage * (state.units == .mmolL ? conversionFactor : 1),
+                median: medianGlucose * (state.units == .mmolL ? conversionFactor : 1),
+                sd: sd * (state.units == .mmolL ? conversionFactor : 1), cv: cv,
+                readings: Double(countReadings) / denominator
+            )
+            return output
+        }
+
+        private func tir(_ glucose_90: FetchedResults<Readings>) -> [(decimal: Decimal, string: String)] {
+            var duration = 1
+
+            switch selectedDuration {
+            case .Today:
+                let minutesSinceMidnight = Calendar.current.component(.hour, from: Date()) * 60 + Calendar.current
+                    .component(.minute, from: Date())
+                duration = minutesSinceMidnight
+            case .Day:
+                duration = 1 * 1440
+            case .Week:
+                duration = 7 * 1440
+            case .Month:
+                duration = 30 * 1440
+            case .Total:
+                duration = 90 * 1440
+            }
+
+            let hypoLimit = Int(state.lowLimit ?? 70)
+            let hyperLimit = Int(state.highLimit ?? 145)
+
+            let timeAgo = Date().addingTimeInterval(-duration.minutes.timeInterval)
+            let glucose = glucose_90.filter({ ($0.date ?? Date()) >= timeAgo })
+
+            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+            let totalReadings = justGlucoseArray.count
+
+            let hyperArray = glucose.filter({ $0.glucose >= hyperLimit })
+            let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
+            let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
+
+            let hypoArray = glucose.filter({ $0.glucose <= hypoLimit })
+            let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
+            let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
+
+            let tir = 100 - (hypoPercentage + hyperPercentage)
+
+            var array: [(decimal: Decimal, string: String)] = []
+            array.append((decimal: Decimal(hypoPercentage), string: "Low"))
+            array.append((decimal: Decimal(tir), string: "NormaL"))
+            array.append((decimal: Decimal(hyperPercentage), string: "High"))
+
+            return array
+        }
+
+        private func colorOfGlucose(_ index: Int) -> Color {
+            let whichIndex = index
+
+            switch whichIndex {
+            case 0:
+                return .red
+            case 1:
+                return .green
+            case 2:
+                return .orange
+            default:
+                return .primary
+            }
+        }
+    }
+}

+ 4 - 4
FreeAPS/Sources/Router/Screen.swift

@@ -29,6 +29,7 @@ enum Screen: Identifiable, Hashable {
     case iconConfig
     case overrideProfilesConfig
     case snooze
+    case statistics
     case watch
 
     var id: Int { String(reflecting: self).hashValue }
@@ -40,10 +41,7 @@ extension Screen {
         case .loading:
             ProgressView()
         case .home:
-            Home.RootView(
-                resolver: resolver,
-                selectedState: .day
-            )
+            Home.RootView(resolver: resolver)
         case .settings:
             Settings.RootView(resolver: resolver)
         case let .configEditor(file):
@@ -96,6 +94,8 @@ extension Screen {
             Snooze.RootView(resolver: resolver)
         case .watch:
             WatchConfig.RootView(resolver: resolver)
+        case .statistics:
+            Stat.RootView(resolver: resolver)
         }
     }
 

+ 0 - 1
FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

@@ -186,7 +186,6 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         addAppBadge(glucose: nil)
 
         let glucose = glucoseStorage.recent()
-
         guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { return }
 
         addAppBadge(glucose: lastGlucose.glucose)