diff --git a/Cartfile.resolved b/Cartfile.resolved index 4c20147..80e4cdb 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ github "tidepool-org/LoopKit" "899f958b50dd22014d97c4730e44266ad8135805" -github "tidepool-org/TidepoolKit" "2a1858cf1040d8b01b6f7357551536417ad55b04" +github "tidepool-org/TidepoolKit" "4f4747ff647d836c5a27cc1b9c275e5717901e83" diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index e463177..966a776 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -32,7 +32,6 @@ A98737CD2788E61400A6A23D /* InsulinType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98737CC2788E61400A6A23D /* InsulinType.swift */; }; A9A53E2F271508D80050C0B1 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A53E2E271508D80050C0B1 /* String.swift */; }; A9D10DB827AB2CCF00814B7B /* SyncAlertObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D10DB727AB2CCF00814B7B /* SyncAlertObject.swift */; }; - A9D1107C242289720091C620 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D1107B242289720091C620 /* HKUnit.swift */; }; A9D1AC9B27B1E046008C5A12 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D1AC9A27B1E046008C5A12 /* Data.swift */; }; A9D1AC9D27B1E3C6008C5A12 /* DoseEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D1AC9C27B1E3C6008C5A12 /* DoseEntry.swift */; }; A9D1AC9F27B1E3D4008C5A12 /* DoseEntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D1AC9E27B1E3D4008C5A12 /* DoseEntryTests.swift */; }; @@ -63,12 +62,15 @@ A9F9F319271A05B100D19374 /* IdentifiableHKDatum.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */; }; B66D1FAC2E6A8D8300471149 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B66D1FAB2E6A8D8300471149 /* Localizable.xcstrings */; }; B66D1FAE2E6A8D8300471149 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B66D1FAD2E6A8D8300471149 /* Localizable.xcstrings */; }; + B40B20CC2CD2AC600027BF35 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */; }; C110888F2A39149100BA4898 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C110888E2A39149100BA4898 /* BuildDetails.swift */; }; C124239D2A58771A00EAC89E /* TidepoolKit in Frameworks */ = {isa = PBXBuildFile; productRef = C124239C2A58771A00EAC89E /* TidepoolKit */; }; C12E4BBA288F2215009C98A2 /* TidepoolServiceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; platformFilter = ios; }; C12E4BBB288F2215009C98A2 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C12E4BBE288F2215009C98A2 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; platformFilter = ios; }; C12E4BBF288F2215009C98A2 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C14559A62C7CF49100541EF1 /* TemporaryScheduleOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */; }; + C1A685432C067E410071C171 /* DeviceLogUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A685422C067E410071C171 /* DeviceLogUploader.swift */; }; C1C9414629F0CB21008D3E05 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9414529F0CB21008D3E05 /* UIImage.swift */; }; C1D0B62929848A460098D215 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62829848A460098D215 /* SettingsView.swift */; }; C1D0B62C29848BEB0098D215 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62B29848BEB0098D215 /* Image.swift */; }; @@ -169,7 +171,6 @@ A9BF371C2418195C008D7F34 /* TidepoolKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A9BF371E24181977008D7F34 /* TidepoolKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A9D10DB727AB2CCF00814B7B /* SyncAlertObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncAlertObject.swift; sourceTree = ""; }; - A9D1107B242289720091C620 /* HKUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; A9D1AC9A27B1E046008C5A12 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; A9D1AC9C27B1E3C6008C5A12 /* DoseEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEntry.swift; sourceTree = ""; }; A9D1AC9E27B1E3D4008C5A12 /* DoseEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseEntryTests.swift; sourceTree = ""; }; @@ -205,6 +206,10 @@ B66D1FAB2E6A8D8300471149 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; B66D1FAD2E6A8D8300471149 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; C110888E2A39149100BA4898 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = ""; }; + B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; + C110888E2A39149100BA4898 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = ""; }; + C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryScheduleOverride.swift; sourceTree = ""; }; + C1A685422C067E410071C171 /* DeviceLogUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLogUploader.swift; sourceTree = ""; }; C1C9414529F0CB21008D3E05 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; C1D0B62829848A460098D215 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; C1D0B62B29848BEB0098D215 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; @@ -272,7 +277,6 @@ A9D1AC9C27B1E3C6008C5A12 /* DoseEntry.swift */, A9752A96270B91E000E50750 /* Double.swift */, A9752A98270B93A700E50750 /* GlucoseRangeSchedule.swift */, - A9D1107B242289720091C620 /* HKUnit.swift */, A98737CC2788E61400A6A23D /* InsulinType.swift */, A98737CA2788DAE400A6A23D /* PersistedPumpEvent.swift */, A9752A9A270B941C00E50750 /* SingleQuantitySchedule.swift */, @@ -285,6 +289,7 @@ A97A60FA243818C900AD69A5 /* TDatum.swift */, A9752A9C270B972D00E50750 /* TimeInterval.swift */, A9D10DB727AB2CCF00814B7B /* SyncAlertObject.swift */, + C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */, ); path = Extensions; sourceTree = ""; @@ -367,6 +372,8 @@ A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */, A913B37B24200C86000805C4 /* Extensions */, B66D1FAD2E6A8D8300471149 /* Localizable.xcstrings */, + A9DAAD4122E7DF9B00E76C9F /* Localizable.strings */, + C1A685422C067E410071C171 /* DeviceLogUploader.swift */, ); path = TidepoolServiceKit; sourceTree = ""; @@ -441,6 +448,7 @@ C1D0B62A29848BD90098D215 /* Extensions */ = { isa = PBXGroup; children = ( + B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */, C1D0B62B29848BEB0098D215 /* Image.swift */, C1C9414529F0CB21008D3E05 /* UIImage.swift */, ); @@ -711,7 +719,6 @@ buildActionMask = 2147483647; files = ( A9309CAF2436C52900E02268 /* StoredGlucoseSample.swift in Sources */, - A9D1107C242289720091C620 /* HKUnit.swift in Sources */, A9752A9D270B972D00E50750 /* TimeInterval.swift in Sources */, A9309CA72435987000E02268 /* SyncCarbObject.swift in Sources */, A9A53E2F271508D80050C0B1 /* String.swift in Sources */, @@ -720,6 +727,7 @@ A9F9F317271A046E00D19374 /* StoredCarbEntry.swift in Sources */, A9D1AC9D27B1E3C6008C5A12 /* DoseEntry.swift in Sources */, A9752A9B270B941C00E50750 /* SingleQuantitySchedule.swift in Sources */, + C1A685432C067E410071C171 /* DeviceLogUploader.swift in Sources */, A9752A93270B766A00E50750 /* StoredDosingDecision.swift in Sources */, A9752A97270B91E000E50750 /* Double.swift in Sources */, C110888F2A39149100BA4898 /* BuildDetails.swift in Sources */, @@ -730,6 +738,7 @@ A98737CD2788E61400A6A23D /* InsulinType.swift in Sources */, A9D1AC9B27B1E046008C5A12 /* Data.swift in Sources */, A9F9F319271A05B100D19374 /* IdentifiableHKDatum.swift in Sources */, + C14559A62C7CF49100541EF1 /* TemporaryScheduleOverride.swift in Sources */, A9057687271F770F0030C3B1 /* IdentifiableDatum.swift in Sources */, A97651752421AA10002EB5D4 /* OSLog.swift in Sources */, A9DAAD3622E7CAC100E76C9F /* TidepoolService.swift in Sources */, @@ -766,6 +775,7 @@ A97651762421AA11002EB5D4 /* OSLog.swift in Sources */, A9DAAD3422E7CA1A00E76C9F /* LocalizedString.swift in Sources */, A9DAAD3922E7DEE000E76C9F /* TidepoolService+UI.swift in Sources */, + B40B20CC2CD2AC600027BF35 /* EnvironmentValues.swift in Sources */, A9DAAD6F22E7EA9700E76C9F /* NibLoadable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -932,7 +942,7 @@ CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -965,7 +975,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, @@ -1044,6 +1054,7 @@ CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -1071,7 +1082,7 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, @@ -1276,6 +1287,233 @@ }; name = Release; }; + B4E7CFA82AD03299009B4DF2 /* Testflight */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_GCD_PERFORMANCE = YES; + CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_ASSIGN_ENUM = YES; + CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES_ERROR; + CLANG_WARN_BOOL_CONVERSION = YES_ERROR; + CLANG_WARN_COMMA = YES_ERROR; + CLANG_WARN_CONSTANT_CONVERSION = YES_ERROR; + CLANG_WARN_CXX0X_EXTENSIONS = YES; + CLANG_WARN_DELETE_NON_VIRTUAL_DTOR = YES_ERROR; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES_ERROR; + CLANG_WARN_FLOAT_CONVERSION = YES_ERROR; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES_ERROR; + CLANG_WARN_MISSING_NOESCAPE = YES_ERROR; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES; + CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_INTERFACE_IVARS = YES_ERROR; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR; + CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES_AGGRESSIVE; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_PRAGMA_PACK = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES_ERROR; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE; + CLANG_WARN_VEXING_PARSE = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES_ERROR; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; + GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_STRICT_SELECTOR_MATCH = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LOCALIZED_STRING_MACRO_NAMES = ( + NSLocalizedString, + CFLocalizedString, + LocalizedString, + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WARNING_CFLAGS = "-Wall"; + }; + name = Testflight; + }; + B4E7CFA92AD03299009B4DF2 /* Testflight */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = TidepoolServiceKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Testflight; + }; + B4E7CFAA2AD03299009B4DF2 /* Testflight */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = TidepoolServiceKitTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Testflight; + }; + B4E7CFAB2AD03299009B4DF2 /* Testflight */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = TidepoolServiceKitUI/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Testflight; + }; + B4E7CFAC2AD03299009B4DF2 /* Testflight */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = TidepoolServiceKitUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Testflight; + }; + B4E7CFAD2AD03299009B4DF2 /* Testflight */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TidepoolServiceKitPlugin/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.tidepool.TidepoolServiceKitPlugin; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = loopplugin; + }; + name = Testflight; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1283,6 +1521,7 @@ isa = XCConfigurationList; buildConfigurations = ( A94AE4EA235A89B5005CA320 /* Debug */, + B4E7CFAD2AD03299009B4DF2 /* Testflight */, A94AE4EB235A89B5005CA320 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1292,6 +1531,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAACF122E7978800E76C9F /* Debug */, + B4E7CFA82AD03299009B4DF2 /* Testflight */, A9DAACF222E7978800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1301,6 +1541,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD1122E7987800E76C9F /* Debug */, + B4E7CFA92AD03299009B4DF2 /* Testflight */, A9DAAD1222E7987800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1310,6 +1551,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD1422E7987800E76C9F /* Debug */, + B4E7CFAA2AD03299009B4DF2 /* Testflight */, A9DAAD1522E7987800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1319,6 +1561,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD2D22E7988900E76C9F /* Debug */, + B4E7CFAB2AD03299009B4DF2 /* Testflight */, A9DAAD2E22E7988900E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1328,6 +1571,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD3022E7988900E76C9F /* Debug */, + B4E7CFAC2AD03299009B4DF2 /* Testflight */, A9DAAD3122E7988900E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme b/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme index 4bd32d3..c9e4db5 100644 --- a/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme +++ b/TidepoolService.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme @@ -1,6 +1,6 @@ 0 { + log.debug("Waiting %{public}@s until next upload", String(timeUntilNextUpload)) + try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds) + } + do { + try await upload(from: nextLogStart!, to: nextLogEnd) + nextLogStart = nextLogEnd + } catch { + log.error("Upload failed: %{public}@", String(describing: error)) + // Upload failed, retry in 5 minutes. + try? await Task.sleep(nanoseconds: TimeInterval(minutes: 5).nanoseconds) + } + } else { + // Haven't been able to talk to backend to find any previous log uploads. Retry in 15 minutes. + try? await Task.sleep(nanoseconds: TimeInterval(minutes: 15).nanoseconds) + } + } + } + + func getMostRecentUploadEndTime() async throws -> Date { + var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date()) + uploadMetadata.sort { a, b in + return a.endAtTime < b.endAtTime + } + if let lastEnd = uploadMetadata.last?.endAtTime { + return lastEnd + } else { + // No previous uploads found in last two days + return Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration) + } + } + + func upload(from start: Date, to end: Date) async throws { + if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) { + if logs.count > 0 { + let data = logs.map({ + entry in + TDeviceLogEntry( + type: entry.type.tidepoolType, + managerIdentifier: entry.managerIdentifier, + deviceIdentifier: entry.deviceIdentifier ?? "unknown", + timestamp: entry.timestamp, + message: entry.message + ) + }) + let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end) + log.debug("Uploaded %d entries from %{public}@ to %{public}@", logs.count, String(describing: start), String(describing: end)) + log.debug("metadata: %{public}@", String(describing: metatdata)) + } else { + log.debug("No device log entries from %{public}@ to %{public}@", String(describing: start), String(describing: end)) + } + } + } +} + +extension TimeInterval { + var nanoseconds: UInt64 { + return UInt64(self * 1e+9) + } +} + +extension DeviceLogEntryType { + var tidepoolType: TDeviceLogEntry.TDeviceLogEntryType { + switch self { + case .send: + return .send + case .receive: + return .receive + case .error: + return .error + case .delegate: + return .delegate + case .delegateResponse: + return .delegateResponse + case .connection: + return .connection + } + } +} diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift index 1b4f4f7..316681c 100644 --- a/TidepoolServiceKit/Extensions/DoseEntry.swift +++ b/TidepoolServiceKit/Extensions/DoseEntry.swift @@ -6,6 +6,7 @@ // Copyright © 2022 LoopKit Authors. All rights reserved. // +import LoopAlgorithm import LoopKit import TidepoolKit @@ -82,8 +83,8 @@ extension DoseEntry: IdentifiableDatum { payload["deliveredUnits"] = datumBasalDeliveredUnits var datum = TAutomatedBasalDatum(time: datumTime, - duration: !isMutable ? datumDuration : 0, - expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil, + duration: datumDuration, + expectedDuration: nil, rate: datumScheduledBasalRate, scheduleName: StoredSettings.activeScheduleNameDefault, insulinFormulation: datumInsulinFormulation) @@ -96,11 +97,9 @@ extension DoseEntry: IdentifiableDatum { } private func dataForBolus(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { - // TODO: revert to using .insulin datum type once fully supported in Tidepool frontend -// if manuallyEntered { -// return dataForBolusManuallyEntered(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) -// } else - if automatic != true { + if manuallyEntered { + return dataForBolusManuallyEntered(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + } else if automatic != true { return dataForBolusManual(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) } else { return dataForBolusAutomatic(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) @@ -209,8 +208,8 @@ extension DoseEntry: IdentifiableDatum { payload["deliveredUnits"] = deliveredUnits var datum = TAutomatedBasalDatum(time: datumTime, - duration: !isMutable ? datumDuration : 0, - expectedDuration: !isMutable && datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil, + duration: datumDuration, + expectedDuration: datumDuration < basalDatumExpectedDuration ? basalDatumExpectedDuration : nil, rate: datumRate, scheduleName: StoredSettings.activeScheduleNameDefault, insulinFormulation: datumInsulinFormulation) @@ -347,3 +346,194 @@ extension TNormalBolusDatum: TypedDatum { extension TInsulinDatum: TypedDatum { static var resolvedType: String { TDatum.DatumType.insulin.rawValue } } + +extension DoseEntry { + + /// Annotates a dose with the context of a history of scheduled basal rates + /// + /// If the dose crosses a schedule boundary, it will be split into multiple doses so each dose has a + /// single scheduled basal rate. + /// + /// - Parameter basalHistory: The history of basal schedule values to apply. Only schedule values overlapping the dose should be included. + /// - Returns: An array of annotated doses + fileprivate func annotated(with basalHistory: [AbsoluteScheduleValue]) -> [DoseEntry] { + + guard type == .tempBasal || type == .suspend, !basalHistory.isEmpty else { + return [self] + } + + if type == .suspend { + guard value == 0 else { + preconditionFailure("suspend with non-zero delivery") + } + } else { + guard unit != .units else { + preconditionFailure("temp basal without rate unsupported") + } + } + + if isMutable { + var newDose = self + let basal = basalHistory.first! + newDose.scheduledBasalRate = LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: basal.value) + return [newDose] + } + + var doses: [DoseEntry] = [] + + for (index, basalItem) in basalHistory.enumerated() { + let startDate: Date + let endDate: Date + + if index == 0 { + startDate = self.startDate + } else { + startDate = basalItem.startDate + } + + if index == basalHistory.count - 1 { + endDate = self.endDate + } else { + endDate = basalHistory[index + 1].startDate + } + + let segmentStartDate = max(startDate, self.startDate) + let segmentEndDate = max(startDate, min(endDate, self.endDate)) + let segmentDuration = segmentEndDate.timeIntervalSince(segmentStartDate) + let segmentPortion = (segmentDuration / duration) + + var annotatedDose = self + annotatedDose.startDate = segmentStartDate + annotatedDose.endDate = segmentEndDate + annotatedDose.scheduledBasalRate = LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: basalItem.value) + + if let deliveredUnits { + annotatedDose.deliveredUnits = deliveredUnits * segmentPortion + } + + doses.append(annotatedDose) + } + + if doses.count > 1 { + for (index, dose) in doses.enumerated() { + if let originalIdentifier = dose.syncIdentifier, index>0 { + doses[index].syncIdentifier = originalIdentifier + "\(index+1)/\(doses.count)" + } + } + } + + return doses + } + +} + + +extension Collection where Element == DoseEntry { + + /// Annotates a sequence of dose entries with the configured basal history + /// + /// Doses which cross time boundaries in the basal rate schedule are split into multiple entries. + /// + /// - Parameter basalHistory: A history of basal rates covering the timespan of these doses. + /// - Returns: An array of annotated dose entries + public func annotated(with basalHistory: [AbsoluteScheduleValue]) -> [DoseEntry] { + var annotatedDoses: [DoseEntry] = [] + + for dose in self { + let basalItems = basalHistory.filterDateRange(dose.startDate, dose.endDate) + annotatedDoses += dose.annotated(with: basalItems) + } + + return annotatedDoses + } + + + /// Assigns an automation status to any dose where automation is not already specified + /// + /// - Parameters: + /// - automationHistory: A history of automation periods. + /// - Returns: An array of doses, with the automation flag set based on automation history. Doses will be split if the automation state changes mid-dose. + + public func overlayAutomationHistory( + _ automationHistory: [AbsoluteScheduleValue] + ) -> [DoseEntry] { + + guard count > 0 else { + return [] + } + + var newEntries = [DoseEntry]() + + var automation = automationHistory + + // Assume automation if doses start before automationHistory + if let firstAutomation = automation.first, firstAutomation.startDate > first!.startDate { + automation.insert(AbsoluteScheduleValue(startDate: first!.startDate, endDate: firstAutomation.startDate, value: true), at: 0) + } + + // Overlay automation periods + func annotateDoseWithAutomation(dose: DoseEntry) { + + var addedCount = 0 + for period in automation { + if period.endDate > dose.startDate && period.startDate < dose.endDate { + var newDose = dose + + if dose.isMutable { + newDose.automatic = period.value + newEntries.append(newDose) + return + } + + newDose.startDate = Swift.max(period.startDate, dose.startDate) + newDose.endDate = Swift.min(period.endDate, dose.endDate) + if let delivered = dose.deliveredUnits { + if dose.duration == 0 || delivered == 0 { + newDose.deliveredUnits = dose.deliveredUnits + } else { + newDose.deliveredUnits = newDose.duration / dose.duration * delivered + } + } + newDose.automatic = period.value + if addedCount > 0 { + newDose.syncIdentifier = "\(dose.syncIdentifierAsString)\(addedCount+1)" + } + newEntries.append(newDose) + addedCount += 1 + } + } + if addedCount == 0 { + // automation history did not cover dose; mark automatic as default + var newDose = dose + newDose.automatic = true + newEntries.append(newDose) + } + } + + for dose in self { + switch dose.type { + case .tempBasal, .basal, .suspend: + if dose.automatic == nil { + annotateDoseWithAutomation(dose: dose) + } else { + newEntries.append(dose) + } + default: + newEntries.append(dose) + break + } + } + return newEntries + } + +} + +extension DoseEntry { + var simpleDesc: String { + let seconds = Int(duration) + let automatic = automatic?.description ?? "na" + return "\(startDate) (\(seconds)s) - \(type) - isMutable:\(isMutable) automatic:\(automatic) value:\(value) delivered:\(String(describing: deliveredUnits)) scheduled:\(String(describing: scheduledBasalRate)) syncId:\(String(describing: syncIdentifier))" + } +} + + diff --git a/TidepoolServiceKit/Extensions/Double.swift b/TidepoolServiceKit/Extensions/Double.swift index 9307ef7..4f55fc4 100644 --- a/TidepoolServiceKit/Extensions/Double.swift +++ b/TidepoolServiceKit/Extensions/Double.swift @@ -6,11 +6,11 @@ // Copyright © 2021 LoopKit Authors. All rights reserved. // -import HealthKit +import LoopAlgorithm import LoopKit extension DoubleRange { - func converted(from: HKUnit, to: HKUnit) -> DoubleRange { + func converted(from: LoopUnit, to: LoopUnit) -> DoubleRange { guard from != to else { return self } @@ -19,10 +19,10 @@ extension DoubleRange { } extension Double { - func converted(from: HKUnit, to: HKUnit) -> Double { + func converted(from: LoopUnit, to: LoopUnit) -> Double { guard from != to else { return self } - return HKQuantity(unit: from, doubleValue: self).doubleValue(for: to) + return LoopQuantity(unit: from, doubleValue: self).doubleValue(for: to) } } diff --git a/TidepoolServiceKit/Extensions/GlucoseRangeSchedule.swift b/TidepoolServiceKit/Extensions/GlucoseRangeSchedule.swift index 6257444..8f6939c 100644 --- a/TidepoolServiceKit/Extensions/GlucoseRangeSchedule.swift +++ b/TidepoolServiceKit/Extensions/GlucoseRangeSchedule.swift @@ -6,11 +6,11 @@ // Copyright © 2021 LoopKit Authors. All rights reserved. // -import HealthKit +import LoopAlgorithm import LoopKit extension GlucoseRangeSchedule { - func items(for unit: HKUnit) -> [RepeatingScheduleValue] { + func items(for unit: LoopUnit) -> [RepeatingScheduleValue] { guard unit != self.unit else { return items } diff --git a/TidepoolServiceKit/Extensions/HKUnit.swift b/TidepoolServiceKit/Extensions/HKUnit.swift deleted file mode 100644 index bf4dd3b..0000000 --- a/TidepoolServiceKit/Extensions/HKUnit.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// HKUnit.swift -// TidepoolServiceKit -// -// Created by Darin Krauss on 3/18/20. -// Copyright © 2020 LoopKit Authors. All rights reserved. -// - -import HealthKit - -extension HKUnit { - public static let milligramsPerDeciliter: HKUnit = { - return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) - }() - - public static let millimolesPerLiter: HKUnit = { - return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter()) - }() - - public static let milligramsPerDeciliterPerMinute: HKUnit = { - return HKUnit.milligramsPerDeciliter.unitDivided(by: .minute()) - }() - - public static let millimolesPerLiterPerMinute: HKUnit = { - return HKUnit.millimolesPerLiter.unitDivided(by: .minute()) - }() - - public static let internationalUnitsPerHour: HKUnit = { - return HKUnit.internationalUnit().unitDivided(by: .hour()) - }() -} diff --git a/TidepoolServiceKit/Extensions/InsulinType.swift b/TidepoolServiceKit/Extensions/InsulinType.swift index d436afc..1437960 100644 --- a/TidepoolServiceKit/Extensions/InsulinType.swift +++ b/TidepoolServiceKit/Extensions/InsulinType.swift @@ -8,6 +8,7 @@ import LoopKit import TidepoolKit +import LoopAlgorithm extension InsulinType { var datum: TInsulinDatum.Formulation { diff --git a/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift b/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift index b926997..489f9c1 100644 --- a/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift +++ b/TidepoolServiceKit/Extensions/PersistedPumpEvent.swift @@ -57,6 +57,8 @@ extension PersistedPumpEvent: IdentifiableDatum { return dataForRewind(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) case .suspend: return dataForSuspend(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + case .timeZoneSync: + return dataForTimeZoneSync(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) default: return [] } @@ -173,6 +175,33 @@ extension PersistedPumpEvent: IdentifiableDatum { origin: origin) return [datum] } + + private func dataForTimeZoneSync(for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { + guard let type = type, + case let .timeZoneSync(fromSecondsFromGMT, toSecondsFromGMT) = type + else { + return [] + } + + let fromTime = formattedDateWithoutTimeZoneOffset(date, for: TimeZone(secondsFromGMT: fromSecondsFromGMT)) + let toTime = formattedDateWithoutTimeZoneOffset(date, for: TimeZone(secondsFromGMT: toSecondsFromGMT)) + var datum = TTimeChangeDeviceEventDatum(time: date, + from: TTimeChangeDeviceEventDatum.Info(time: fromTime), + to: TTimeChangeDeviceEventDatum.Info(time: toTime), + method: .manual) + let origin = datumOrigin(for: resolvedIdentifier(for: TTimeChangeDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) + datum = datum.adornWith(id: datumId(for: userId, type: TTimeChangeDeviceEventDatum.self), + payload: datumPayload, + origin: origin) + return [datum] + } + + private func formattedDateWithoutTimeZoneOffset(_ date: Date, for timeZone: TimeZone?) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + dateFormatter.timeZone = timeZone + return dateFormatter.string(from: date) + } private var datumTime: Date { dose?.startDate ?? date } @@ -223,3 +252,7 @@ extension TReservoirChangeDeviceEventDatum: TypedDatum { extension TStatusDeviceEventDatum: TypedDatum { static var resolvedType: String { "\(TDatum.DatumType.deviceEvent.rawValue)/\(TDeviceEventDatum.SubType.status.rawValue)" } } + +extension TTimeChangeDeviceEventDatum: TypedDatum { + static var resolvedType: String { "\(TDatum.DatumType.deviceEvent.rawValue)/\(TDeviceEventDatum.SubType.timeChange.rawValue)" } +} diff --git a/TidepoolServiceKit/Extensions/SingleQuantitySchedule.swift b/TidepoolServiceKit/Extensions/SingleQuantitySchedule.swift index 5576b2d..e83c3f7 100644 --- a/TidepoolServiceKit/Extensions/SingleQuantitySchedule.swift +++ b/TidepoolServiceKit/Extensions/SingleQuantitySchedule.swift @@ -6,11 +6,11 @@ // Copyright © 2021 LoopKit Authors. All rights reserved. // -import HealthKit +import LoopAlgorithm import LoopKit extension SingleQuantitySchedule { - func items(for unit: HKUnit) -> [RepeatingScheduleValue] { + func items(for unit: LoopUnit) -> [RepeatingScheduleValue] { guard unit != self.unit else { return items } diff --git a/TidepoolServiceKit/Extensions/StoredDosingDecision.swift b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift index 7710caf..6c963d4 100644 --- a/TidepoolServiceKit/Extensions/StoredDosingDecision.swift +++ b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift @@ -6,7 +6,7 @@ // Copyright © 2021 LoopKit Authors. All rights reserved. // -import HealthKit +import LoopAlgorithm import LoopKit import TidepoolKit @@ -126,7 +126,7 @@ extension StoredDosingDecision: IdentifiableDatum { guard let originalCarbEntry = originalCarbEntry else { return nil } - let carbohydrate = TDosingDecisionDatum.Nutrition.Carbohydrate(net: originalCarbEntry.quantity.doubleValue(for: .gram()), units: .grams) + let carbohydrate = TDosingDecisionDatum.Nutrition.Carbohydrate(net: originalCarbEntry.quantity.doubleValue(for: .gram), units: .grams) let nutrition = TDosingDecisionDatum.Nutrition(carbohydrate: carbohydrate, estimatedAbsorptionDuration: originalCarbEntry.absorptionTime) return TDosingDecisionDatum.Food(time: originalCarbEntry.startDate, nutrition: nutrition) } @@ -135,7 +135,7 @@ extension StoredDosingDecision: IdentifiableDatum { guard let carbEntry = carbEntry else { return nil } - let carbohydrate = TDosingDecisionDatum.Nutrition.Carbohydrate(net: carbEntry.quantity.doubleValue(for: .gram()), units: .grams) + let carbohydrate = TDosingDecisionDatum.Nutrition.Carbohydrate(net: carbEntry.quantity.doubleValue(for: .gram), units: .grams) let nutrition = TDosingDecisionDatum.Nutrition(carbohydrate: carbohydrate, estimatedAbsorptionDuration: carbEntry.absorptionTime) return TDosingDecisionDatum.Food(time: carbEntry.startDate, nutrition: nutrition) } @@ -152,7 +152,7 @@ extension StoredDosingDecision: IdentifiableDatum { guard let carbsOnBoard = carbsOnBoard else { return nil } - return TDosingDecisionDatum.CarbohydratesOnBoard(time: carbsOnBoard.startDate, amount: carbsOnBoard.quantity.doubleValue(for: .gram()).rounded(decimalPlaces: 4)) + return TDosingDecisionDatum.CarbohydratesOnBoard(time: carbsOnBoard.startDate, amount: carbsOnBoard.quantity.doubleValue(for: .gram).rounded(decimalPlaces: 4)) } private var datumInsulinOnBoard: TDosingDecisionDatum.InsulinOnBoard? { @@ -298,7 +298,7 @@ fileprivate extension StoredDosingDecision.ControllerStatus.BatteryState { } fileprivate extension PumpManagerStatus.BasalDeliveryState { - var datum: TPumpStatusDatum.BasalDelivery { + var datum: TPumpStatusDatum.BasalDelivery? { switch self { case .active(let at): return TPumpStatusDatum.BasalDelivery(state: .scheduled, time: at) @@ -314,6 +314,8 @@ fileprivate extension PumpManagerStatus.BasalDeliveryState { return TPumpStatusDatum.BasalDelivery(state: .suspended, time: at) case .resuming: return TPumpStatusDatum.BasalDelivery(state: .resuming) + case .pumpInoperable: + return nil } } } @@ -343,8 +345,8 @@ fileprivate extension DoseEntry { } } -fileprivate extension HKQuantity { - func doubleValueClampedAndRounded(for unit: HKUnit) -> Double { +fileprivate extension LoopQuantity { + func doubleValueClampedAndRounded(for unit: LoopUnit) -> Double { let value = doubleValue(for: unit) switch unit { case .milligramsPerDeciliter: diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift index e3993b6..1ba9b58 100644 --- a/TidepoolServiceKit/Extensions/StoredSettings.swift +++ b/TidepoolServiceKit/Extensions/StoredSettings.swift @@ -6,7 +6,6 @@ // Copyright © 2021 LoopKit Authors. All rights reserved. // -import HealthKit import LoopKit import TidepoolKit @@ -19,10 +18,7 @@ import TidepoolKit - dosingEnabled Bool TPumpSettingsDatum.automatedDelivery - glucoseTargetRangeSchedule GlucoseRangeSchedule? TPumpSettingsDatum.bloodGlucoseTargetSchedules["Default"] - preMealTargetRange ClosedRange? TPumpSettingsDatum.bloodGlucoseTargetPreprandial - - workoutTargetRange ClosedRange? TPumpSettingsDatum.bloodGlucoseTargetPhysicalActivity - overridePresets [TemporaryScheduleOverridePreset]? TPumpSettingsDatum.overridePresets - - scheduleOverride TemporaryScheduleOverride? TPumpSettingsOverrideDeviceEventDatum.* - - preMealOverride TemporaryScheduleOverride? TPumpSettingsOverrideDeviceEventDatum.* - maximumBasalRatePerHour Double? TPumpSettingsDatum.basal.rateMaximum.value - maximumBolus Double? TPumpSettingsDatum.bolus.amountMaximum.value - suspendThreshold GlucoseThreshold? TPumpSettingsDatum.bloodGlucoseSafetyLimit @@ -38,7 +34,6 @@ import TidepoolKit - syncIdentifier UUID .id, .origin, .payload["syncIdentifier"] Notes: - - The active override (scheduleOverride or preMealOverride) are stored in TPumpSettingsOverrideDeviceEventDatum. - Assumes same time zone for basalRateSchedule, glucoseTargetRangeSchedule, carbRatioSchedule, insulinSensitivitySchedule. - StoredSettings.notificationSettings.carPlaySetting is not included as it is unneeded by backend. - StoredSettings.notificationSettings.showPreviewsSetting is not included as it is unneeded by backend. @@ -68,7 +63,6 @@ extension StoredSettings: IdentifiableDatum { manufacturers: datumCGMManufacturers, model: datumCGMModel, name: datumCGMName, - serialNumber: datumCGMSerialNumber, softwareVersion: datumCGMSoftwareVersion, transmitterId: nil, // TODO: https://tidepool.atlassian.net/browse/LOOP-3929 units: datumCGMUnits, @@ -85,11 +79,9 @@ extension StoredSettings: IdentifiableDatum { func datumPumpSettings(for userId: String, hostIdentifier: String, hostVersion: String) -> TPumpSettingsDatum { let datum = TPumpSettingsDatum(time: datumTime, activeScheduleName: datumPumpActiveScheduleName, - automatedDelivery: datumPumpAutomatedDelivery, basal: datumPumpBasal, basalRateSchedules: datumPumpBasalRateSchedules, bloodGlucoseSafetyLimit: datumPumpBloodGlucoseSafetyLimit, - bloodGlucoseTargetPhysicalActivity: datumPumpBloodGlucoseTargetPhysicalActivity, bloodGlucoseTargetPreprandial: datumPumpBloodGlucoseTargetPreprandial, bloodGlucoseTargetSchedules: datumPumpBloodGlucoseTargetSchedules, bolus: datumPumpBolus, @@ -105,7 +97,6 @@ extension StoredSettings: IdentifiableDatum { name: datumPumpName, overridePresets: datumPumpOverridePresets, scheduleTimeZoneOffset: datumPumpScheduleTimeZoneOffset, - serialNumber: datumPumpSerialNumber, softwareVersion: datumPumpSoftwareVersion, units: datumPumpUnits) let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) @@ -116,29 +107,6 @@ extension StoredSettings: IdentifiableDatum { origin: origin) } - func datumPumpSettingsOverrideDeviceEvent(for userId: String, hostIdentifier: String, hostVersion: String) -> TPumpSettingsOverrideDeviceEventDatum? { - guard let activeOverride = activeOverride else { - return nil - } - let datum = TPumpSettingsOverrideDeviceEventDatum(time: activeOverride.datumTime, - overrideType: activeOverride.datumOverrideType, - overridePreset: activeOverride.datumOverridePreset, - method: activeOverride.datumMethod, - duration: activeOverride.datumDuration, - expectedDuration: activeOverride.datumExpectedDuration, - bloodGlucoseTarget: activeOverride.datumBloodGlucoseTarget, - basalRateScaleFactor: activeOverride.datumBasalRateScaleFactor, - carbohydrateRatioScaleFactor: activeOverride.datumCarbohydrateRatioScaleFactor, - insulinSensitivityScaleFactor: activeOverride.datumInsulinSensitivityScaleFactor, - units: activeOverride.datumUnits) - let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsOverrideDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) - return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsOverrideDeviceEventDatum.self), - timeZone: datumTimeZone, - timeZoneOffset: datumTimeZoneOffset, - payload: datumPayload, - origin: origin) - } - var syncIdentifierAsString: String { syncIdentifier.uuidString } private var datumTime: Date { date } @@ -171,8 +139,6 @@ extension StoredSettings: IdentifiableDatum { private var datumCGMName: String? { cgmDevice?.name } - private var datumCGMSerialNumber: String? { cgmDevice?.localIdentifier } - private var datumCGMSoftwareVersion: String? { cgmDevice?.softwareVersion } private var datumCGMUnits: TCGMSettingsDatum.Units { .milligramsPerDeciliter } @@ -181,8 +147,6 @@ extension StoredSettings: IdentifiableDatum { return Self.activeScheduleNameDefault } - private var datumPumpAutomatedDelivery: Bool { dosingEnabled } - private var datumPumpBasal: TPumpSettingsDatum.Basal? { guard let maximumBasalRatePerHour = maximumBasalRatePerHour else { return nil @@ -204,14 +168,6 @@ extension StoredSettings: IdentifiableDatum { return suspendThreshold.convertTo(unit: .milligramsPerDeciliter).value } - private var datumPumpBloodGlucoseTargetPhysicalActivity: TPumpSettingsDatum.BloodGlucoseTarget? { - guard let workoutTargetRange = workoutTargetRange else { - return nil - } - return TPumpSettingsDatum.BloodGlucoseTarget(low: workoutTargetRange.lowerBound.doubleValue(for: .milligramsPerDeciliter), - high: workoutTargetRange.upperBound.doubleValue(for: .milligramsPerDeciliter)) - } - private var datumPumpBloodGlucoseTargetPreprandial: TPumpSettingsDatum.BloodGlucoseTarget? { guard let preMealTargetRange = preMealTargetRange else { return nil @@ -238,7 +194,7 @@ extension StoredSettings: IdentifiableDatum { guard let carbRatioSchedule = carbRatioSchedule else { return nil } - return [Self.activeScheduleNameDefault: carbRatioSchedule.items(for: .gram()).map { TPumpSettingsDatum.CarbohydrateRatioStart(start: $0.startTime, amount: $0.value) }] + return [Self.activeScheduleNameDefault: carbRatioSchedule.items(for: .gram).map { TPumpSettingsDatum.CarbohydrateRatioStart(start: $0.startTime, amount: $0.value) }] } private var datumPumpDisplay: TPumpSettingsDatum.Display? { @@ -304,7 +260,7 @@ extension StoredSettings: IdentifiableDatum { private var datumPumpName: String? { pumpDevice?.name } private var datumPumpOverridePresets: [String: TPumpSettingsDatum.OverridePreset]? { - guard let overridePresets = overridePresets, !overridePresets.isEmpty else { + guard !overridePresets.isEmpty else { return nil } return overridePresets.reduce(into: [:]) { $0[$1.name] = $1.datum } @@ -319,8 +275,6 @@ extension StoredSettings: IdentifiableDatum { return TimeInterval(seconds: scheduleTimeZone.secondsFromGMT(for: date)) } - private var datumPumpSerialNumber: String? { pumpDevice?.localIdentifier } - private var datumPumpSoftwareVersion: String? { pumpDevice?.softwareVersion } private var datumPumpUnits: TPumpSettingsDatum.Units { @@ -333,19 +287,6 @@ extension StoredSettings: IdentifiableDatum { return dictionary } - private var activeOverride: TemporaryScheduleOverride? { - switch (preMealOverride, scheduleOverride) { - case (let preMealOverride?, nil): - return preMealOverride - case (nil, let scheduleOverride?): - return scheduleOverride - case (let preMealOverride?, let scheduleOverride?): - return preMealOverride.scheduledEndDate > date ? preMealOverride : scheduleOverride - case (nil, nil): - return nil - } - } - public static var activeScheduleNameDefault: String { "Default" } } @@ -414,7 +355,7 @@ fileprivate extension NotificationSettings.AlertStyle { } } -fileprivate extension TemporaryScheduleOverridePreset { +fileprivate extension TemporaryPreset { var datum: TPumpSettingsDatum.OverridePreset { return TPumpSettingsDatum.OverridePreset(abbreviation: datumAbbreviation, duration: datumDuration, @@ -424,83 +365,11 @@ fileprivate extension TemporaryScheduleOverridePreset { insulinSensitivityScaleFactor: settings.datumInsulinSensitivityScaleFactor) } - var datumAbbreviation: String? { symbol } - - var datumDuration: TimeInterval? { duration.isFinite ? duration.timeInterval : nil } -} - -fileprivate extension TemporaryScheduleOverride { - var datumTime: Date { startDate } - - var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType { context.datumOverrideType } - - var datumOverridePreset: String? { - guard case .preset(let preset) = context else { - return nil - } - return preset.name + var datumAbbreviation: String? { + symbol?.textualRepresentation?.isEmpty == true ? nil : symbol?.textualRepresentation } - - var datumMethod: TPumpSettingsOverrideDeviceEventDatum.Method? { .manual } - - var datumDuration: TimeInterval? { - switch duration { - case .finite(let interval): - return interval - case .indefinite: - return nil - } - } - - var datumExpectedDuration: TimeInterval? { nil } - - var datumBloodGlucoseTarget: TPumpSettingsOverrideDeviceEventDatum.BloodGlucoseTarget? { settings.datumBloodGlucoseTarget } - - var datumBasalRateScaleFactor: Double? { settings.datumBasalRateScaleFactor } - - var datumCarbohydrateRatioScaleFactor: Double? { settings.datumCarbohydrateRatioScaleFactor } - - var datumInsulinSensitivityScaleFactor: Double? { settings.datumInsulinSensitivityScaleFactor } - - var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? { settings.datumUnits } -} -fileprivate extension TemporaryScheduleOverride.Context { - var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType { - switch self { - case .preMeal: - return .preprandial - case .legacyWorkout: - return .physicalActivity - case .preset(_): - return .preset - case .custom: - return .custom - } - } -} - -fileprivate extension TemporaryScheduleOverrideSettings { - var datumBloodGlucoseTarget: TPumpSettingsDatum.BloodGlucoseTarget? { - guard let targetRange = targetRange else { - return nil - } - return TPumpSettingsDatum.BloodGlucoseTarget(low: targetRange.lowerBound.doubleValue(for: .milligramsPerDeciliter), - high: targetRange.upperBound.doubleValue(for: .milligramsPerDeciliter)) - } - - var datumBasalRateScaleFactor: Double? { basalRateMultiplier } - - var datumCarbohydrateRatioScaleFactor: Double? { carbRatioMultiplier } - - var datumInsulinSensitivityScaleFactor: Double? { insulinSensitivityMultiplier } - - var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? { - guard targetRange != nil else { - return nil - } - return TPumpSettingsOverrideDeviceEventDatum.Units(bloodGlucose: .milligramsPerDeciliter) - } + var datumDuration: TimeInterval? { duration.isFinite ? duration.timeInterval : nil } } extension TCGMSettingsDatum: TypedDatum { diff --git a/TidepoolServiceKit/Extensions/SyncCarbObject.swift b/TidepoolServiceKit/Extensions/SyncCarbObject.swift index 5203776..d758ad0 100644 --- a/TidepoolServiceKit/Extensions/SyncCarbObject.swift +++ b/TidepoolServiceKit/Extensions/SyncCarbObject.swift @@ -55,7 +55,7 @@ extension SyncCarbObject: IdentifiableHKDatum { } private var datumCarbohydrate: TFoodDatum.Nutrition.Carbohydrate { - return TFoodDatum.Nutrition.Carbohydrate(net: quantity.doubleValue(for: .gram()), units: .grams) + return TFoodDatum.Nutrition.Carbohydrate(net: quantity.doubleValue(for: .gram), units: .grams) } private var datumPayload: TDictionary? { diff --git a/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift b/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift new file mode 100644 index 0000000..edf263e --- /dev/null +++ b/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift @@ -0,0 +1,124 @@ +// +// TemporaryScheduleOverride.swift +// TidepoolServiceKit +// +// Created by Pete Schwamb on 8/26/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation +import TidepoolKit +import LoopKit + +fileprivate extension TemporaryScheduleOverride.Context { + var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType { + switch self { + case .preMeal: + return .preprandial + case .activity: + return .physicalActivity + case .preset(_): + return .preset + case .custom: + return .custom + } + } +} + +extension TemporaryPresetSettings { + var datumBloodGlucoseTarget: TPumpSettingsDatum.BloodGlucoseTarget? { + guard let targetRange = targetRange else { + return nil + } + return TPumpSettingsDatum.BloodGlucoseTarget(low: targetRange.lowerBound.doubleValue(for: .milligramsPerDeciliter), + high: targetRange.upperBound.doubleValue(for: .milligramsPerDeciliter)) + } + + var datumBasalRateScaleFactor: Double? { basalRateMultiplier } + + var datumCarbohydrateRatioScaleFactor: Double? { carbRatioMultiplier } + + var datumInsulinSensitivityScaleFactor: Double? { insulinSensitivityMultiplier } + + var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? { + guard targetRange != nil else { + return nil + } + return TPumpSettingsOverrideDeviceEventDatum.Units(bloodGlucose: .milligramsPerDeciliter) + } +} + +extension TemporaryScheduleOverride: IdentifiableDatum { + + var syncIdentifierAsString: String { syncIdentifier.uuidString } + + func datum(for userId: String, hostIdentifier: String, hostVersion: String) -> TDatum { + let datum = TPumpSettingsOverrideDeviceEventDatum(time: datumTime, + overrideType: datumOverrideType, + overridePreset: datumOverridePreset, + method: datumMethod, + duration: datumDuration, + expectedDuration: datumExpectedDuration, + bloodGlucoseTarget: datumBloodGlucoseTarget, + basalRateScaleFactor: datumBasalRateScaleFactor, + carbohydrateRatioScaleFactor: datumCarbohydrateRatioScaleFactor, + insulinSensitivityScaleFactor: datumInsulinSensitivityScaleFactor, + units: datumUnits) + let origin = datumOrigin(for: resolvedIdentifier(for: TPumpSettingsOverrideDeviceEventDatum.self), hostIdentifier: hostIdentifier, hostVersion: hostVersion) + return datum.adornWith(id: datumId(for: userId, type: TPumpSettingsOverrideDeviceEventDatum.self), + payload: datumPayload, + origin: origin) + } + + private var datumPayload: TDictionary { + var dictionary = TDictionary() + dictionary["syncIdentifier"] = syncIdentifierAsString + return dictionary + } + + var datumTime: Date { startDate } + + var datumOverrideType: TPumpSettingsOverrideDeviceEventDatum.OverrideType { context.datumOverrideType } + + var datumOverridePreset: String? { + guard case .preset(let preset) = context else { + return nil + } + guard preset.name.isEmpty == false else { + // This shouldn't happen, but the backend will reject the data if not set + return "unnamed" + } + return preset.name + } + + var datumMethod: TPumpSettingsOverrideDeviceEventDatum.Method? { .manual } + + var datumDuration: TimeInterval? { + switch duration { + case .finite(let interval): + return interval + case .indefinite: + return nil + } + } + + var datumExpectedDuration: TimeInterval? { nil } + + var datumBloodGlucoseTarget: TPumpSettingsOverrideDeviceEventDatum.BloodGlucoseTarget? { settings.datumBloodGlucoseTarget } + + var datumBasalRateScaleFactor: Double? { settings.datumBasalRateScaleFactor } + + var datumCarbohydrateRatioScaleFactor: Double? { settings.datumCarbohydrateRatioScaleFactor } + + var datumInsulinSensitivityScaleFactor: Double? { settings.datumInsulinSensitivityScaleFactor } + + var datumUnits: TPumpSettingsOverrideDeviceEventDatum.Units? { settings.datumUnits } + +} + +extension TemporaryScheduleOverride { + var selectors: [TDatum.Selector] { + return [datumSelector(for: TPumpSettingsOverrideDeviceEventDatum.self)] + } +} + diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 805eaa1..9871dfd 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -32,8 +32,10 @@ public protocol SessionStorage { } public final class TidepoolService: Service, TAPIObserver, ObservableObject { - - public static let pluginIdentifier = "TidepoolService" + + public static let serviceIdentifier: String = "TidepoolService" + + public var pluginIdentifier: String { Self.serviceIdentifier } public static let localizedTitle = LocalizedString("Tidepool", comment: "The title of the Tidepool service") @@ -46,40 +48,59 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { public weak var stateDelegate: StatefulPluggableDelegate? + public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate? { + didSet { + Task { + await setDeviceLogUploaderDelegate() + } + } + } + public lazy var sessionStorage: SessionStorage = KeychainManager() public let tapi: TAPI = TAPI(clientId: BuildDetails.default.tidepoolServiceClientId, redirectURL: BuildDetails.default.tidepoolServiceRedirectURL) - public private (set) var error: Error? + public private(set) var error: Error? private let id: String - private var lastControllerSettingsDatum: TControllerSettingsDatum? private var lastCGMSettingsDatum: TCGMSettingsDatum? private var lastPumpSettingsDatum: TPumpSettingsDatum? - private var lastPumpSettingsOverrideDeviceEventDatum: TPumpSettingsOverrideDeviceEventDatum? - private var hostIdentifier: String? private var hostVersion: String? - private let log = OSLog(category: pluginIdentifier) + private let log = OSLog(category: "TidepoolService") private let tidepoolKitLog = OSLog(category: "TidepoolKit") + private var deviceLogUploader: DeviceLogUploader? + + public var isDependency: Bool = false + + private func setDeviceLogUploaderDelegate() async { + await deviceLogUploader?.setDelegate(remoteDataServiceDelegate) + } + public init(hostIdentifier: String, hostVersion: String) { self.id = UUID().uuidString self.hostIdentifier = hostIdentifier self.hostVersion = hostVersion Task { - await tapi.setLogging(self) - await tapi.addObserver(self) + await finishSetup() } } + public func finishSetup() async { + await tapi.setLogging(self) + await tapi.addObserver(self) + deviceLogUploader = DeviceLogUploader(api: tapi) + await setDeviceLogUploaderDelegate() + } + public init?(rawState: RawStateValue) { self.isOnboarded = true // Assume when restoring from state, that we're onboarded guard let id = rawState["id"] as? String else { @@ -93,12 +114,10 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { self.lastControllerSettingsDatum = (rawState["lastControllerSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TControllerSettingsDatum.self, from: $0) } self.lastCGMSettingsDatum = (rawState["lastCGMSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TCGMSettingsDatum.self, from: $0) } self.lastPumpSettingsDatum = (rawState["lastPumpSettingsDatum"] as? Data).flatMap { try? Self.decoder.decode(TPumpSettingsDatum.self, from: $0) } - self.lastPumpSettingsOverrideDeviceEventDatum = (rawState["lastPumpSettingsOverrideDeviceEventDatum"] as? Data).flatMap { try? Self.decoder.decode(TPumpSettingsOverrideDeviceEventDatum.self, from: $0) } self.session = try sessionStorage.getSession(for: sessionService) Task { await tapi.setSession(session) - await tapi.setLogging(self) - await tapi.addObserver(self) + await finishSetup() } } catch let error { tidepoolKitLog.error("Error initializing TidepoolService %{public}@", error.localizedDescription) @@ -116,13 +135,20 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { rawValue["lastControllerSettingsDatum"] = lastControllerSettingsDatum.flatMap { try? Self.encoder.encode($0) } rawValue["lastCGMSettingsDatum"] = lastCGMSettingsDatum.flatMap { try? Self.encoder.encode($0) } rawValue["lastPumpSettingsDatum"] = lastPumpSettingsDatum.flatMap { try? Self.encoder.encode($0) } - rawValue["lastPumpSettingsOverrideDeviceEventDatum"] = lastPumpSettingsOverrideDeviceEventDatum.flatMap { try? Self.encoder.encode($0) } return rawValue } public var isOnboarded = false // No distinction between created and onboarded + + public func markAsDepedency(_ isDependency: Bool) { + self.isDependency = isDependency + } @Published public var session: TSession? + + public var isDemoAccount: Bool { + session?.userRoles.contains("demo") ?? false + } public func apiDidUpdateSession(_ session: TSession?) { guard session != self.session else { @@ -147,10 +173,18 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { let content = Alert.Content(title: LocalizedString("Tidepool Service Authorization", comment: "The title for an alert generated when TidepoolService is no longer authorized."), body: LocalizedString("Tidepool service is no longer authorized. Please navigate to Tidepool Service settings and reauthenticate.", comment: "The body text for an alert generated when TidepoolService is no longer authorized."), acknowledgeActionButtonLabel: LocalizedString("OK", comment: "Alert acknowledgment OK button")) - serviceDelegate?.issueAlert(Alert(identifier: Alert.Identifier(managerIdentifier: pluginIdentifier, - alertIdentifier: "authentication-needed"), - foregroundContent: content, backgroundContent: content, - trigger: .immediate)) + Task { + await serviceDelegate?.issueAlert( + Alert( + identifier: Alert + .Identifier(managerIdentifier: pluginIdentifier, + alertIdentifier: "authentication-needed"), + foregroundContent: content, + backgroundContent: content, + trigger: .immediate + ) + ) + } } } @@ -277,83 +311,81 @@ extension TidepoolService: TLogging { extension TidepoolService: RemoteDataService { - public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride], completion: @escaping (Result) -> Void) { - // TODO: Implement - completion(.success(true)) + public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride]) async throws { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { + throw TidepoolServiceError.configuration + } + + let _ = try await createData(updated.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + let _ = try await deleteData(withSelectors: deleted.flatMap { $0.selectors }) } public var alertDataLimit: Int? { return 1000 } - public func uploadAlertData(_ stored: [SyncAlertObject], completion: @escaping (_ result: Result) -> Void) { + public func uploadAlertData(_ stored: [SyncAlertObject]) async throws { guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - completion(.failure(TidepoolServiceError.configuration)) - return - } - Task { - do { - let result = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) - completion(.success(result)) - } catch { - completion(.failure(error)) - } + throw TidepoolServiceError.configuration } + + let _ = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) } public var carbDataLimit: Int? { return 1000 } - public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject], completion: @escaping (Result) -> Void) { + public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject]) async throws { guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } - Task { - do { - let createdUploaded = try await createData(created.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) - let updatedUploaded = try await updateData(updated.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) - let deletedUploaded = try await deleteData(withSelectors: deleted.compactMap { $0.selector }) - completion(.success(createdUploaded || updatedUploaded || deletedUploaded)) - } catch { - completion(.failure(error)) - } - } + let _ = try await createData(created.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + let _ = try await updateData(updated.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + let _ = try await deleteData(withSelectors: deleted.compactMap { $0.selector }) } public var doseDataLimit: Int? { return 1000 } - public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry], completion: @escaping (_ result: Result) -> Void) { - guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - completion(.failure(TidepoolServiceError.configuration)) - return + private func annotateDoses(_ doses: [DoseEntry]) async throws -> [DoseEntry] { + guard !doses.isEmpty else { + return [] } - Task { - do { - let createdUploaded = try await createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) - let deletedUploaded = try await deleteData(withSelectors: deleted.flatMap { $0.selectors }) - completion(.success(createdUploaded || deletedUploaded)) - } catch { - completion(.failure(error)) - } + guard let remoteDataServiceDelegate else { + throw TidepoolServiceError.configuration } + + let start = doses.map { $0.startDate }.min()! + let end = doses.map { $0.endDate }.max()! + + let basal = try await remoteDataServiceDelegate.getBasalHistory(startDate: start, endDate: end) + let dosesWithBasal = doses.annotated(with: basal) + + let automationHistory = try await remoteDataServiceDelegate.automationHistory(from: start, to: end) + return dosesWithBasal.overlayAutomationHistory(automationHistory) + + } + + public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry]) async throws { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { + throw TidepoolServiceError.configuration + } + + // Syncidentifiers may be changed + let annotatedCreated = try await annotateDoses(created) + let _ = try await createData(annotatedCreated.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + + // annotating these so we get the correct syncIdentifiers to delete + let annotatedDeleted = try await annotateDoses(deleted) + let _ = try await deleteData(withSelectors: annotatedDeleted.flatMap { $0.selectors }) } public var dosingDecisionDataLimit: Int? { return 50 } // Each can be up to 20K bytes of serialized JSON, target ~1M or less - public func uploadDosingDecisionData(_ stored: [StoredDosingDecision], completion: @escaping (_ result: Result) -> Void) { + public func uploadDosingDecisionData(_ stored: [StoredDosingDecision]) async throws { guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } - Task { - do { - let result = try await createData(calculateDosingDecisionData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) - completion(.success(result)) - } catch { - completion(.failure(error)) - } - } + let _ = try await createData(calculateDosingDecisionData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion)) } func calculateDosingDecisionData(_ stored: [StoredDosingDecision], for userId: String, hostIdentifier: String, hostVersion: String) -> [TDatum] { @@ -404,79 +436,47 @@ extension TidepoolService: RemoteDataService { public var glucoseDataLimit: Int? { return 1000 } - public func uploadGlucoseData(_ stored: [StoredGlucoseSample], completion: @escaping (Result) -> Void) { + public func uploadGlucoseData(_ stored: [StoredGlucoseSample]) async throws { guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } - Task { - do { - let result = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) - completion(.success(result)) - } catch { - completion(.failure(error)) - } - } + let _ = try await createData(stored.compactMap { $0.datum(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) } public var pumpDataEventLimit: Int? { return 1000 } - public func uploadPumpEventData(_ stored: [PersistedPumpEvent], completion: @escaping (_ result: Result) -> Void) { + public func uploadPumpEventData(_ stored: [PersistedPumpEvent]) async throws { guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } - Task { - do { - let result = try await createData(stored.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) - completion(.success(result)) - } catch { - completion(.failure(error)) - } - } + let _ = try await createData(stored.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) } public var settingsDataLimit: Int? { return 400 } // Each can be up to 2.5K bytes of serialized JSON, target ~1M or less - public func uploadSettingsData(_ stored: [StoredSettings], completion: @escaping (_ result: Result) -> Void) { + public func uploadSettingsData(_ stored: [StoredSettings]) async throws { guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } - let (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum, lastPumpSettingsOverrideDeviceEventDatum) = calculateSettingsData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + let (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum) = calculateSettingsData(stored, for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) - Task { - do { - let createdUploaded = try await createData(created) - let updatedUploaded = try await updateData(updated) - self.lastControllerSettingsDatum = lastControllerSettingsDatum - self.lastCGMSettingsDatum = lastCGMSettingsDatum - self.lastPumpSettingsDatum = lastPumpSettingsDatum - self.lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum - self.completeUpdate() - completion(.success(createdUploaded || updatedUploaded)) - } catch { - completion(.failure(error)) - } - } + let _ = try await createData(created) + let _ = try await updateData(updated) + self.lastControllerSettingsDatum = lastControllerSettingsDatum + self.lastCGMSettingsDatum = lastCGMSettingsDatum + self.lastPumpSettingsDatum = lastPumpSettingsDatum + self.completeUpdate() } - func calculateSettingsData(_ stored: [StoredSettings], for userId: String, hostIdentifier: String, hostVersion: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?, TPumpSettingsOverrideDeviceEventDatum?) { + func calculateSettingsData(_ stored: [StoredSettings], for userId: String, hostIdentifier: String, hostVersion: String) -> ([TDatum], [TDatum], TControllerSettingsDatum?, TCGMSettingsDatum?, TPumpSettingsDatum?) { var created: [TDatum] = [] - var updated: [TDatum] = [] + let updated: [TDatum] = [] var lastControllerSettingsDatum = lastControllerSettingsDatum var lastCGMSettingsDatum = lastCGMSettingsDatum var lastPumpSettingsDatum = lastPumpSettingsDatum - var lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum - - // A StoredSettings can generate a TPumpSettingsDatum and an optional TPumpSettingsOverrideDeviceEventDatum if there is an - // enabled override. Only upload the TPumpSettingsDatum or TPumpSettingsOverrideDeviceEventDatum if they have CHANGED. - // If the TPumpSettingsOverrideDeviceEventDatum has changed, then also re-upload the previous uploaded - // TPumpSettingsOverrideDeviceEventDatum with an updated duration and potentially expected duration, but only if the - // duration is calculated to be ended early. stored.forEach { @@ -491,15 +491,11 @@ extension TidepoolService: RemoteDataService { let pumpSettingsDatum = $0.datumPumpSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) let pumpSettingsDatumIsEffectivelyEquivalent = TPumpSettingsDatum.areEffectivelyEquivalent(old: lastPumpSettingsDatum, new: pumpSettingsDatum) - let pumpSettingsOverrideDeviceEventDatum = $0.datumPumpSettingsOverrideDeviceEvent(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) - let pumpSettingsOverrideDeviceEventDatumIsEffectivelyEquivalent = TPumpSettingsOverrideDeviceEventDatum.areEffectivelyEquivalent(old: lastPumpSettingsOverrideDeviceEventDatum, new: pumpSettingsOverrideDeviceEventDatum) - // Associate the data var controllerSettingsAssociations: [TAssociation] = [] var cgmSettingsAssociations: [TAssociation] = [] var pumpSettingsAssociations: [TAssociation] = [] - var pumpSettingsOverrideDeviceEventAssociations: [TAssociation] = [] if let controllerSettingsDatum = controllerSettingsDatumIsEffectivelyEquivalent ? lastControllerSettingsDatum : controllerSettingsDatum { let association = TAssociation(type: .datum, id: controllerSettingsDatum.id!, reason: "controllerSettings") @@ -515,13 +511,11 @@ extension TidepoolService: RemoteDataService { let association = TAssociation(type: .datum, id: pumpSettingsDatum.id!, reason: "pumpSettings") controllerSettingsAssociations.append(association) cgmSettingsAssociations.append(association) - pumpSettingsOverrideDeviceEventAssociations.append(association) } controllerSettingsDatum.append(associations: controllerSettingsAssociations) cgmSettingsDatum.append(associations: cgmSettingsAssociations) pumpSettingsDatum.append(associations: pumpSettingsAssociations) - pumpSettingsOverrideDeviceEventDatum?.append(associations: pumpSettingsOverrideDeviceEventAssociations) // Upload and update the data, if necessary @@ -539,27 +533,9 @@ extension TidepoolService: RemoteDataService { created.append(pumpSettingsDatum) lastPumpSettingsDatum = pumpSettingsDatum } - - if !pumpSettingsOverrideDeviceEventDatumIsEffectivelyEquivalent { - - // If we need to update the duration of the last override, then do so - if let lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum, - lastPumpSettingsOverrideDeviceEventDatum.updateDuration(basedUpon: pumpSettingsOverrideDeviceEventDatum?.time ?? pumpSettingsDatum.time) { - - // If it isn't already being created, then update it - if !created.contains(where: { $0 === lastPumpSettingsOverrideDeviceEventDatum }) { - updated.append(lastPumpSettingsOverrideDeviceEventDatum) - } - } - - if let pumpSettingsOverrideDeviceEventDatum = pumpSettingsOverrideDeviceEventDatum { - created.append(pumpSettingsOverrideDeviceEventDatum) - } - lastPumpSettingsOverrideDeviceEventDatum = pumpSettingsOverrideDeviceEventDatum - } } - return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum, lastPumpSettingsOverrideDeviceEventDatum) + return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum) } private func createData(_ data: [TDatum]) async throws -> Bool { @@ -615,9 +591,8 @@ extension TidepoolService: RemoteDataService { } } - public func uploadCgmEventData(_ stored: [LoopKit.PersistedCgmEvent], completion: @escaping (Result) -> Void) { + public func uploadCgmEventData(_ stored: [PersistedCgmEvent]) async throws { // TODO: Upload sensor/transmitter changes - completion(.success(false)) } public func remoteNotificationWasReceived(_ notification: [String: AnyObject]) async throws { @@ -683,7 +658,6 @@ extension TCGMSettingsDatum: EffectivelyEquivalent { self.manufacturers == other.manufacturers && self.model == other.model && self.name == other.name && - self.serialNumber == other.serialNumber && self.softwareVersion == other.softwareVersion && self.transmitterId == other.transmitterId && self.units == other.units && @@ -702,7 +676,6 @@ extension TCGMSettingsDatum: EffectivelyEquivalent { manufacturers == nil && model == nil && name == nil && - serialNumber == nil && softwareVersion == nil && transmitterId == nil && defaultAlerts == nil && @@ -719,7 +692,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { // All TDatum properties can be ignored for this datum type func isEffectivelyEquivalent(to other: TPumpSettingsDatum) -> Bool { return self.activeScheduleName == other.activeScheduleName && - self.automatedDelivery == other.automatedDelivery && self.basal == other.basal && self.basalRateSchedule == other.basalRateSchedule && self.basalRateSchedules == other.basalRateSchedules && @@ -743,7 +715,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { self.name == other.name && self.overridePresets == other.overridePresets && self.scheduleTimeZoneOffset == other.scheduleTimeZoneOffset && - self.serialNumber == other.serialNumber && self.softwareVersion == other.softwareVersion && self.units == other.units } @@ -751,7 +722,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { // Ignore units as they are always specified var isEffectivelyEmpty: Bool { return activeScheduleName == nil && - automatedDelivery == nil && basal == nil && basalRateSchedule == nil && basalRateSchedules == nil && @@ -775,58 +745,10 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { name == nil && overridePresets == nil && scheduleTimeZoneOffset == nil && - serialNumber == nil && softwareVersion == nil } } -extension TPumpSettingsOverrideDeviceEventDatum: EffectivelyEquivalent { - - // All TDatum properties can be ignored EXCEPT time for this datum type - // Time is gather from the actual scheduled override and NOT the StoredSettings so it is valid and necessary for comparison - func isEffectivelyEquivalent(to other: TPumpSettingsOverrideDeviceEventDatum) -> Bool { - return self.time == other.time && - self.overrideType == other.overrideType && - self.overridePreset == other.overridePreset && - self.method == other.method && - self.duration == other.duration && - self.expectedDuration == other.expectedDuration && - self.bloodGlucoseTarget == other.bloodGlucoseTarget && - self.basalRateScaleFactor == other.basalRateScaleFactor && - self.carbohydrateRatioScaleFactor == other.carbohydrateRatioScaleFactor && - self.insulinSensitivityScaleFactor == other.insulinSensitivityScaleFactor && - self.units == other.units - } - - var isEffectivelyEmpty: Bool { - return overrideType == nil && - overridePreset == nil && - method == nil && - duration == nil && - expectedDuration == nil && - bloodGlucoseTarget == nil && - basalRateScaleFactor == nil && - carbohydrateRatioScaleFactor == nil && - insulinSensitivityScaleFactor == nil && - units == nil - } - - func updateDuration(basedUpon endTime: Date?) -> Bool { - guard let endTime = endTime, let time = time, endTime > time else { - return false - } - - let updatedDuration = time.distance(to: endTime) - guard duration == nil || updatedDuration < duration! else { - return false - } - - self.expectedDuration = duration - self.duration = updatedDuration - return true - } -} - fileprivate extension TDosingDecisionDatum { // Ignore reason and units as they are always specified diff --git a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift index 56d6a9c..74df2dc 100644 --- a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift +++ b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift @@ -10,9 +10,9 @@ import Foundation import XCTest import Foundation -import HealthKit import LoopKit import TidepoolKit +import LoopAlgorithm @testable import TidepoolServiceKit class DoseEntryDataTests: XCTestCase { @@ -25,10 +25,11 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:25:23Z")!, value: 0.75, unit: .units, + decisionId: nil, deliveredUnits: nil, description: "Test Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .novolog, automatic: true, manuallyEntered: false) @@ -38,7 +39,6 @@ class DoseEntryDataTests: XCTestCase { { "deliveryType" : "automated", "duration" : 1500000, - "expectedDuration" : 1800000, "id" : "f839af02f6832d7c81d636dbbbadbc01", "insulinFormulation" : { "simple" : { @@ -72,6 +72,7 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: nil, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -118,6 +119,7 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: 3.5, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -163,6 +165,7 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: 3.5, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -213,6 +216,7 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: 3.5, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -258,6 +262,7 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: 3.5, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -308,6 +313,7 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:30:23Z")!, value: 0, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: nil, description: "Test Resume Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -329,10 +335,11 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:30:23Z")!, value: 0, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: nil, description: "Test Suspend Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, automatic: true, manuallyEntered: false) @@ -372,10 +379,11 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:20:23Z")!, value: 1.5, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: 0.5, description: "Test Temp Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, automatic: false, manuallyEntered: false) @@ -424,10 +432,11 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:20:23Z")!, value: 1.5, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: 0.5, description: "Test Temp Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, automatic: false, manuallyEntered: false, @@ -481,10 +490,11 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:20:23Z")!, value: 1.5, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: 0.5, description: "Test Temp Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, manuallyEntered: false) let data = doseEntry.data(for: "2B03D96C-6F5D-4140-99CD-80C3E64D6011", hostIdentifier: hostIdentifier, hostVersion: hostVersion) @@ -533,10 +543,11 @@ class DoseEntryDataTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:20:23Z")!, value: 1.5, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: 0.5, description: "Test Temp Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, manuallyEntered: false, isMutable: true) @@ -550,7 +561,8 @@ class DoseEntryDataTests: XCTestCase { } ], "deliveryType" : "automated", - "duration" : 0, + "duration" : 1200000, + "expectedDuration" : 1800000, "id" : "f839af02f6832d7c81d636dbbbadbc01", "insulinFormulation" : { "simple" : { @@ -600,10 +612,11 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:25:23Z")!, value: 0.75, unit: .units, + decisionId: nil, deliveredUnits: nil, description: "Test Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .novolog, automatic: true, manuallyEntered: false) @@ -616,6 +629,7 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: nil, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -632,6 +646,7 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: 3.5, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -648,6 +663,7 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: 3.5, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -664,6 +680,7 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:30:23Z")!, value: 0, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: nil, description: "Test Resume Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -680,10 +697,11 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:30:23Z")!, value: 0, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: nil, description: "Test Suspend Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, automatic: true, manuallyEntered: false) @@ -696,10 +714,11 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:20:23Z")!, value: 1.5, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: 0.5, description: "Test Temp Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, automatic: false, manuallyEntered: false) @@ -712,14 +731,144 @@ class DoseEntrySelectorTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:20:23Z")!, value: 1.5, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: 0.5, description: "Test Temp Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, manuallyEntered: false) XCTAssertEqual(doseEntry.selectors, [TDatum.Selector(origin: TDatum.Selector.Origin(id: "ab0a722d639669875017a899a5214677:basal/automated"))]) } + func testOverlayAutomationHistory_NoAutomationHistory() { + let doses: [DoseEntry] = [ + DoseEntry(type: .basal, startDate: Date(), endDate: Date().addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false) + ] + let result = doses.overlayAutomationHistory([]) + + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result[0].automatic, true) // Default to true when no automation history + } + + func testOverlayAutomationHistory_SingleAutomationPeriod() { + let now = Date() + let doses: [DoseEntry] = [ + DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false) + ] + let automationHistory: [AbsoluteScheduleValue] = [ + AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(3600), value: false) + ] + + let result = doses.overlayAutomationHistory(automationHistory) + + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result[0].automatic, false) + } + + func testOverlayAutomationHistory_MultipleAutomationPeriods() { + let now = Date() + let doses: [DoseEntry] = [ + DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false) + ] + let automationHistory: [AbsoluteScheduleValue] = [ + AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(1800), value: false), + AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(3600), value: true) + ] + + let result = doses.overlayAutomationHistory(automationHistory) + + XCTAssertEqual(result.count, 2) + XCTAssertEqual(result[0].automatic, false) + XCTAssertEqual(result[1].automatic, true) + } + + func testOverlayAutomationHistory_PartialOverlap() { + let now = Date() + let doses: [DoseEntry] = [ + DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false) + ] + let automationHistory: [AbsoluteScheduleValue] = [ + AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(4800), value: false) + ] + + let result = doses.overlayAutomationHistory(automationHistory) + + XCTAssertEqual(result.count, 2) + XCTAssertEqual(result[0].automatic, true) + XCTAssertEqual(result[1].automatic, false) + } + + + func testOverlayAutomationHistory_NonBasalDoses() { + let now = Date() + let doses: [DoseEntry] = [ + DoseEntry(type: .bolus, startDate: now, endDate: now.addingTimeInterval(300), value: 2.0, unit: .unitsPerHour, decisionId: nil, automatic: nil, manuallyEntered: false, isMutable: false), + DoseEntry(type: .basal, startDate: now.addingTimeInterval(300), endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, automatic: nil, manuallyEntered: false, isMutable: false) + ] + let automationHistory: [AbsoluteScheduleValue] = [ + AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(3600), value: false) + ] + + let result = doses.overlayAutomationHistory(automationHistory) + + XCTAssertEqual(result.count, 2) + XCTAssertNil(result[0].automatic) // Bolus dose should remain unchanged + XCTAssertEqual(result[1].automatic, false) + } + + func testOverlayAutomationHistory_PreexistingAutomationFlag() { + let now = Date() + let doses: [DoseEntry] = [ + DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, automatic: true, manuallyEntered: false, isMutable: false) + ] + let automationHistory: [AbsoluteScheduleValue] = [ + AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(3600), value: false) + ] + + let result = doses.overlayAutomationHistory(automationHistory) + + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result[0].automatic, true) // Should not change preexisting automation flag + } + + func testOverlayAutomationHistory_DeliveredUnitsAdjustment() { + let now = Date() + let doses: [DoseEntry] = [ + DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, deliveredUnits: 1.0, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: false) + ] + let automationHistory: [AbsoluteScheduleValue] = [ + AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(1800), value: false), + AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(3600), value: true) + ] + + let result = doses.overlayAutomationHistory(automationHistory) + + XCTAssertEqual(result.count, 2) + XCTAssertEqual(result[0].deliveredUnits!, 0.5, accuracy: 0.001) + XCTAssertEqual(result[0].automatic, false) + XCTAssertEqual(result[1].deliveredUnits!, 0.5, accuracy: 0.001) + XCTAssertEqual(result[1].automatic, true) + } + + func testOverlayAutomationHistory_MutableDose() { + let now = Date() + let doses: [DoseEntry] = [ + DoseEntry(type: .basal, startDate: now, endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, decisionId: nil, deliveredUnits: 1.0, syncIdentifier: "test", automatic: nil, manuallyEntered: false, isMutable: true) + ] + let automationHistory: [AbsoluteScheduleValue] = [ + AbsoluteScheduleValue(startDate: now, endDate: now.addingTimeInterval(1800), value: false), + AbsoluteScheduleValue(startDate: now.addingTimeInterval(1800), endDate: now.addingTimeInterval(3600), value: true) + ] + + let result = doses.overlayAutomationHistory(automationHistory) + + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result[0].deliveredUnits!, 1, accuracy: 0.001) + XCTAssertEqual(result[0].automatic, false) + XCTAssertEqual(result[0].duration, TimeInterval(hours: 1)) + } + + private static let dateFormatter = ISO8601DateFormatter() } diff --git a/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift b/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift index 348a7d3..4d37449 100644 --- a/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift +++ b/TidepoolServiceKitTests/Extensions/PersistedPumpEventTests.swift @@ -8,7 +8,7 @@ import XCTest import Foundation -import HealthKit +import LoopAlgorithm import LoopKit @testable import TidepoolServiceKit @@ -256,10 +256,11 @@ class PersistedPumpEventTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:25:23Z")!, value: 0.75, unit: .units, + decisionId: nil, deliveredUnits: nil, description: "Test Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .novolog, automatic: true, manuallyEntered: false), @@ -286,6 +287,7 @@ class PersistedPumpEventTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:00:53Z")!, value: 4.25, unit: .units, + decisionId: nil, deliveredUnits: 3.5, description: "Test Bolus Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", @@ -665,10 +667,11 @@ class PersistedPumpEventTests: XCTestCase { endDate: Self.dateFormatter.date(from: "2020-01-02T03:20:23Z")!, value: 1.5, unit: .unitsPerHour, + decisionId: nil, deliveredUnits: 0.5, description: "Test Temp Basal Dose", syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", - scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), + scheduledBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: 2.0), insulinType: .fiasp, automatic: true, manuallyEntered: false), diff --git a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift index 78ca2ed..040b2ab 100644 --- a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift @@ -10,6 +10,7 @@ import XCTest import HealthKit import LoopKit import TidepoolKit +import LoopAlgorithm @testable import TidepoolServiceKit class StoredDosingDecisionTests: XCTestCase { @@ -138,7 +139,7 @@ class StoredDosingDecisionTests: XCTestCase { "amount" : 1.25 }, "requestedBolus" : { - "amount" : 0.80000000000000004 + "amount" : 0.8 }, "smbg" : { "time" : "2020-05-14T22:09:00.000Z", @@ -249,7 +250,7 @@ fileprivate extension StoredDosingDecision { let reason = "test" let settings = StoredDosingDecision.Settings(syncIdentifier: UUID(uuidString: "2B03D96C-6F5D-4140-99CD-80C3E64D6011")!) let scheduleOverride = TemporaryScheduleOverride(context: .preMeal, - settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, + settings: TemporaryPresetSettings(unit: .milligramsPerDeciliter, targetRange: DoubleRange(minValue: 80.0, maxValue: 90.0), insulinNeedsScaleFactor: 1.5), @@ -285,13 +286,13 @@ fileprivate extension StoredDosingDecision { let lastReservoirValue = StoredDosingDecision.LastReservoirValue(startDate: dateFormatter.date(from: "2020-05-14T22:07:19Z")!, unitVolume: 113.3) let historicalGlucose = [HistoricalGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:29:15Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 117.3)), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 117.3)), HistoricalGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:33:15Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 119.5)), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 119.5)), HistoricalGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:38:15Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 121.8))] + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 121.8))] let originalCarbEntry = StoredCarbEntry(startDate: dateFormatter.date(from: "2020-01-02T03:00:23Z")!, - quantity: HKQuantity(unit: .gram(), doubleValue: 19), + quantity: LoopQuantity(unit: .gram, doubleValue: 19), uuid: UUID(uuidString: "18CF3948-0B3D-4B12-8BFE-14986B0E6784")!, provenanceIdentifier: "com.loopkit.loop", syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010", @@ -302,7 +303,7 @@ fileprivate extension StoredDosingDecision { userCreatedDate: dateFormatter.date(from: "2020-05-14T22:06:12Z")!, userUpdatedDate: nil) let carbEntry = StoredCarbEntry(startDate: dateFormatter.date(from: "2020-01-02T03:00:23Z")!, - quantity: HKQuantity(unit: .gram(), doubleValue: 29), + quantity: LoopQuantity(unit: .gram, doubleValue: 29), uuid: UUID(uuidString: "135CDABE-9343-7242-4233-1020384789AE")!, provenanceIdentifier: "com.loopkit.loop", syncIdentifier: "2B03D96C-6F5D-4140-99CD-80C3E64D6010", @@ -317,10 +318,10 @@ fileprivate extension StoredDosingDecision { syncIdentifier: "d3876f59-adb3-4a4f-8b29-315cda22062e", syncVersion: 1, startDate: dateFormatter.date(from: "2020-05-14T22:09:00Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 400), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 400), condition: .aboveRange, trend: .downDownDown, - trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -10.2), + trendRate: LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -10.2), isDisplayOnly: false, wasUserEntered: true, device: HKDevice(name: "Device Name", @@ -345,18 +346,17 @@ fileprivate extension StoredDosingDecision { start: dateFormatter.date(from: "2020-05-14T21:12:17Z")!, end: dateFormatter.date(from: "2020-05-14T23:12:17Z")!)) let predictedGlucose = [PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:43:15Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 123.3)), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 123.3)), PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:48:15Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 125.5)), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 125.5)), PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T22:53:15Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 127.8))] + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 127.8))] let tempBasalRecommendation = TempBasalRecommendation(unitsPerHour: 0.75, duration: .minutes(30)) - let automaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: tempBasalRecommendation, bolusUnits: 1.25) + let automaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: tempBasalRecommendation, direction: .increase, bolusUnits: 1.25) let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 1.2, - pendingInsulin: 0.75, - notice: .predictedGlucoseBelowTarget(minGlucose: PredictedGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T23:03:15Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 75.5)))), + notice: .predictedGlucoseBelowTarget(minGlucose: SimpleGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T23:03:15Z")!, + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 75.5)))), date: dateFormatter.date(from: "2020-05-14T22:38:16Z")!) let manualBolusRequested = 0.8 let warnings: [Issue] = [Issue(id: "one"), diff --git a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift index 18641d5..25e93be 100644 --- a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift @@ -7,7 +7,7 @@ // import XCTest -import HealthKit +import LoopAlgorithm import TidepoolKit import LoopKit @testable import TidepoolServiceKit @@ -19,7 +19,7 @@ class StoredGlucoseSampleTests: XCTestCase { syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", syncVersion: 1, startDate: Self.dateFormatter.date(from: "2020-01-02T03:00:23Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 123), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 123), condition: nil, trend: nil, trendRate: nil, @@ -57,7 +57,7 @@ class StoredGlucoseSampleTests: XCTestCase { syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", syncVersion: 2, startDate: Self.dateFormatter.date(from: "2020-01-02T03:00:23Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 167), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 167), condition: nil, trend: nil, trendRate: nil, @@ -95,10 +95,10 @@ class StoredGlucoseSampleTests: XCTestCase { syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", syncVersion: 3, startDate: Self.dateFormatter.date(from: "2020-01-02T03:00:23Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 123), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 123), condition: nil, trend: .flat, - trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 0.1), + trendRate: LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 0.1), isDisplayOnly: false, wasUserEntered: false, device: nil, @@ -119,7 +119,7 @@ class StoredGlucoseSampleTests: XCTestCase { }, "time" : "2020-01-02T03:00:23.000Z", "trend" : "constant", - "trendRate" : 0.10000000000000001, + "trendRate" : 0.1, "type" : "cbg", "units" : "mg/dL", "value" : 123 @@ -134,10 +134,10 @@ class StoredGlucoseSampleTests: XCTestCase { syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", syncVersion: 4, startDate: Self.dateFormatter.date(from: "2020-01-02T03:00:23Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 40.0), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 40.0), condition: .belowRange, trend: .down, - trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -1.0), + trendRate: LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: -1.0), isDisplayOnly: false, wasUserEntered: false, device: nil, @@ -180,10 +180,10 @@ class StoredGlucoseSampleTests: XCTestCase { syncIdentifier: "18CF3948-0B3D-4B12-8BFE-14986B0E6784", syncVersion: 5, startDate: Self.dateFormatter.date(from: "2020-01-02T03:00:23Z")!, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 400.0), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 400.0), condition: .aboveRange, trend: .upUp, - trendRate: HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 4.0), + trendRate: LoopQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: 4.0), isDisplayOnly: false, wasUserEntered: false, device: nil, diff --git a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift index c08ce84..f53d1be 100644 --- a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift @@ -8,6 +8,7 @@ import XCTest import HealthKit +import LoopAlgorithm import LoopKit import TidepoolKit @testable import TidepoolServiceKit @@ -78,7 +79,6 @@ class StoredSettingsTests: XCTestCase { "payload" : { "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E" }, - "serialNumber" : "CGM Local Identifier", "softwareVersion" : "CGM Software Version", "time" : "2020-05-14T22:48:15.000Z", "timezone" : "America/Los_Angeles", @@ -95,7 +95,6 @@ class StoredSettingsTests: XCTestCase { XCTAssertEqual(String(data: data, encoding: .utf8), """ { "activeSchedule" : "Default", - "automatedDelivery" : true, "basal" : { "rateMaximum" : { "units" : "Units/hour", @@ -119,10 +118,6 @@ class StoredSettingsTests: XCTestCase { ] }, "bgSafetyLimit" : 75, - "bgTargetPhysicalActivity" : { - "high" : 160, - "low" : 150 - }, "bgTargetPreprandial" : { "high" : 90, "low" : 80 @@ -231,7 +226,6 @@ class StoredSettingsTests: XCTestCase { "payload" : { "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E" }, - "serialNumber" : "Pump Local Identifier", "softwareVersion" : "Pump Software Version", "time" : "2020-05-14T22:48:15.000Z", "timezone" : "America/Los_Angeles", @@ -247,42 +241,6 @@ class StoredSettingsTests: XCTestCase { ) } - func testDatumPumpSettingsOverrideDeviceEvent() { - let data = try! Self.encoder.encode(StoredSettings.test.datumPumpSettingsOverrideDeviceEvent(for: "1234567890", hostIdentifier: "Loop", hostVersion: "1.2.3")) - XCTAssertEqual(String(data: data, encoding: .utf8), """ -{ - "basalRateScaleFactor" : 0.5, - "bgTarget" : { - "high" : 90, - "low" : 80 - }, - "carbRatioScaleFactor" : 2, - "id" : "f89ad59a42430ab89dd2eab3a3e4df84", - "insulinSensitivityScaleFactor" : 2, - "method" : "manual", - "origin" : { - "id" : "2A67A303-1234-4CB8-1234-79498265368E:deviceEvent/pumpSettingsOverride", - "name" : "Loop", - "type" : "application", - "version" : "1.2.3" - }, - "overrideType" : "preprandial", - "payload" : { - "syncIdentifier" : "2A67A303-1234-4CB8-1234-79498265368E" - }, - "subType" : "pumpSettingsOverride", - "time" : "2020-05-14T14:38:39.000Z", - "timezone" : "America/Los_Angeles", - "timezoneOffset" : -420, - "type" : "deviceEvent", - "units" : { - "bg" : "mg/dL" - } -} -""" - ) - } - private static let encoder: JSONEncoder = { let encoder = JSONEncoder.tidepool encoder.outputFormatting.insert(.prettyPrinted) @@ -304,30 +262,13 @@ fileprivate extension StoredSettings { start: dateFormatter.date(from: "2020-05-14T12:48:15Z")!, end: dateFormatter.date(from: "2020-05-14T14:48:15Z")!)) let preMealTargetRange = DoubleRange(minValue: 80.0, maxValue: 90.0).quantityRange(for: .milligramsPerDeciliter) - let workoutTargetRange = DoubleRange(minValue: 150.0, maxValue: 160.0).quantityRange(for: .milligramsPerDeciliter) - let overridePresets = [TemporaryScheduleOverridePreset(id: UUID(uuidString: "2A67A303-5203-4CB8-8263-79498265368E")!, + let overridePresets = [TemporaryPreset(id: "2A67A303-5203-4CB8-8263-79498265368E", symbol: "🍎", name: "Apple", - settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, + settings: TemporaryPresetSettings(unit: .milligramsPerDeciliter, targetRange: DoubleRange(minValue: 130.0, maxValue: 140.0), insulinNeedsScaleFactor: 2.0), duration: .finite(.minutes(60)))] - let scheduleOverride = TemporaryScheduleOverride(context: .preMeal, - settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, - targetRange: DoubleRange(minValue: 110.0, maxValue: 120.0), - insulinNeedsScaleFactor: 1.5), - startDate: dateFormatter.date(from: "2020-05-14T14:48:19Z")!, - duration: .finite(.minutes(60)), - enactTrigger: .remote("127.0.0.1"), - syncIdentifier: UUID(uuidString: "2A67A303-1234-4CB8-8263-79498265368E")!) - let preMealOverride = TemporaryScheduleOverride(context: .preMeal, - settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, - targetRange: DoubleRange(minValue: 80.0, maxValue: 90.0), - insulinNeedsScaleFactor: 0.5), - startDate: dateFormatter.date(from: "2020-05-14T14:38:39Z")!, - duration: .indefinite, - enactTrigger: .local, - syncIdentifier: UUID(uuidString: "2A67A303-5203-1234-8263-79498265368E")!) let maximumBasalRatePerHour = 3.5 let maximumBolus = 10.0 let suspendThreshold = GlucoseThreshold(unit: .milligramsPerDeciliter, value: 75.0) @@ -342,7 +283,7 @@ fileprivate extension StoredSettings { RepeatingScheduleValue(startTime: .hours(3), value: 40.0), RepeatingScheduleValue(startTime: .hours(15), value: 50.0)], timeZone: scheduleTimeZone) - let carbRatioSchedule = CarbRatioSchedule(unit: .gram(), + let carbRatioSchedule = CarbRatioSchedule(unit: .gram, dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 15.0), RepeatingScheduleValue(startTime: .hours(9), value: 14.0), RepeatingScheduleValue(startTime: .hours(20), value: 18.0)], @@ -383,17 +324,14 @@ fileprivate extension StoredSettings { softwareVersion: "Pump Software Version", localIdentifier: "Pump Local Identifier", udiDeviceIdentifier: "Pump UDI Device Identifier") - let bloodGlucoseUnit = HKUnit.milligramsPerDeciliter + let bloodGlucoseUnit = LoopUnit.milligramsPerDeciliter return StoredSettings(date: dateFormatter.date(from: "2020-05-14T22:48:15Z")!, controllerTimeZone: controllerTimeZone, dosingEnabled: dosingEnabled, glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, preMealTargetRange: preMealTargetRange, - workoutTargetRange: workoutTargetRange, overridePresets: overridePresets, - scheduleOverride: scheduleOverride, - preMealOverride: preMealOverride, maximumBasalRatePerHour: maximumBasalRatePerHour, maximumBolus: maximumBolus, suspendThreshold: suspendThreshold, diff --git a/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift b/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift index 8dea646..9818c1e 100644 --- a/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift +++ b/TidepoolServiceKitTests/Extensions/SyncCarbObjectTests.swift @@ -8,7 +8,6 @@ import Foundation import XCTest -import HealthKit import TidepoolKit import LoopKit @testable import TidepoolServiceKit diff --git a/TidepoolServiceKitTests/TidepoolServiceTests.swift b/TidepoolServiceKitTests/TidepoolServiceTests.swift index 7ace7f1..0d8d70d 100644 --- a/TidepoolServiceKitTests/TidepoolServiceTests.swift +++ b/TidepoolServiceKitTests/TidepoolServiceTests.swift @@ -52,7 +52,7 @@ class TidepoolServiceTests: XCTestCase { let settings = [StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), cgmDevice: HKDevice(name: "CGM #1"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 3) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[0] as! TControllerSettingsDatum).associations!.count, 2) @@ -70,7 +70,6 @@ class TidepoolServiceTests: XCTestCase { XCTAssertEqual(lastControllerSettings!.id, created[0].id) XCTAssertEqual(lastCGMSettings!.id, created[1].id) XCTAssertEqual(lastPumpSettings!.id, created[2].id) - XCTAssertNil(lastPumpSettingsOverride) } func testCalculateSettingsDataUpdateControllerSettings() { @@ -80,7 +79,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"), cgmDevice: HKDevice(name: "CGM #1"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 4) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -93,7 +92,6 @@ class TidepoolServiceTests: XCTestCase { XCTAssertEqual(lastControllerSettings!.id, created[3].id) XCTAssertEqual(lastCGMSettings!.id, created[1].id) XCTAssertEqual(lastPumpSettings!.id, created[2].id) - XCTAssertNil(lastPumpSettingsOverride) } func testCalculateSettingsDataUpdateCGMSettings() { @@ -103,7 +101,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), cgmDevice: HKDevice(name: "CGM #2"), pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 4) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -116,7 +114,6 @@ class TidepoolServiceTests: XCTestCase { XCTAssertEqual(lastControllerSettings!.id, created[0].id) XCTAssertEqual(lastCGMSettings!.id, created[3].id) XCTAssertEqual(lastPumpSettings!.id, created[2].id) - XCTAssertNil(lastPumpSettingsOverride) } func testCalculateSettingsDataUpdatePumpSettings() { @@ -126,7 +123,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), cgmDevice: HKDevice(name: "CGM #1"), pumpDevice: HKDevice(name: "Pump #2"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 4) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -139,7 +136,6 @@ class TidepoolServiceTests: XCTestCase { XCTAssertEqual(lastControllerSettings!.id, created[0].id) XCTAssertEqual(lastCGMSettings!.id, created[1].id) XCTAssertEqual(lastPumpSettings!.id, created[3].id) - XCTAssertNil(lastPumpSettingsOverride) } func testCalculateSettingsDataUpdateMultipleOne() { @@ -149,7 +145,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"), cgmDevice: HKDevice(name: "CGM #2"), pumpDevice: HKDevice(name: "Pump #2"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 6) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -170,7 +166,6 @@ class TidepoolServiceTests: XCTestCase { XCTAssertEqual(lastControllerSettings!.id, created[3].id) XCTAssertEqual(lastCGMSettings!.id, created[4].id) XCTAssertEqual(lastPumpSettings!.id, created[5].id) - XCTAssertNil(lastPumpSettingsOverride) } func testCalculateSettingsDataUpdateMultipleMultiple() { @@ -186,7 +181,7 @@ class TidepoolServiceTests: XCTestCase { StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #2"), cgmDevice: HKDevice(name: "CGM #2"), pumpDevice: HKDevice(name: "Pump #2"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") + let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") XCTAssertEqual(created.count, 6) XCTAssertEqual((created[0] as! TControllerSettingsDatum).device!.name, "Controller #1") XCTAssertEqual((created[1] as! TCGMSettingsDatum).name, "CGM #1") @@ -207,69 +202,8 @@ class TidepoolServiceTests: XCTestCase { XCTAssertEqual(lastControllerSettings!.id, created[3].id) XCTAssertEqual(lastCGMSettings!.id, created[4].id) XCTAssertEqual(lastPumpSettings!.id, created[5].id) - XCTAssertNil(lastPumpSettingsOverride) } - func testCalculateSettingsDataPumpOverrideSingle() { - let scheduleOverride = TemporaryScheduleOverride() - let settings = [StoredSettings(pumpDevice: HKDevice(name: "Pump #1")), - StoredSettings(scheduleOverride: scheduleOverride, - pumpDevice: HKDevice(name: "Pump #1")), - StoredSettings(scheduleOverride: scheduleOverride, - controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), - pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") - XCTAssertEqual(created.count, 3) - XCTAssertEqual((created[0] as! TPumpSettingsDatum).name, "Pump #1") - XCTAssertNil((created[0] as! TPumpSettingsDatum).associations) - XCTAssertNil((created[1] as! TPumpSettingsOverrideDeviceEventDatum).duration) - XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations!.count, 1) - XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations![0].id, created[0].id) - XCTAssertEqual((created[2] as! TControllerSettingsDatum).device!.name, "Controller #1") - XCTAssertTrue(updated.isEmpty) - XCTAssertEqual(lastControllerSettings!.id, created[2].id) - XCTAssertNil(lastCGMSettings) - XCTAssertEqual(lastPumpSettings!.id, created[0].id) - XCTAssertEqual(lastPumpSettingsOverride!.id, created[1].id) - } - - func testCalculateSettingsDataPumpOverrideMultipleAllCreated() { - let settings = [StoredSettings(pumpDevice: HKDevice(name: "Pump #1")), - StoredSettings(scheduleOverride: TemporaryScheduleOverride(duration: .minutes(30)), - pumpDevice: HKDevice(name: "Pump #1")), - StoredSettings(preMealOverride: TemporaryScheduleOverride(), - pumpDevice: HKDevice(name: "Pump #1")), - StoredSettings(pumpDevice: HKDevice(name: "Pump #1")), - StoredSettings(controllerDevice: StoredSettings.ControllerDevice(name: "Controller #1"), - pumpDevice: HKDevice(name: "Pump #1"))] - let (created, updated, lastControllerSettings, lastCGMSettings, lastPumpSettings, lastPumpSettingsOverride) = tidepoolService.calculateSettingsData(settings, for: userID, hostIdentifier: "Loop", hostVersion: "1.2.3") - XCTAssertEqual(created.count, 4) - XCTAssertEqual((created[0] as! TPumpSettingsDatum).name, "Pump #1") - XCTAssertNil((created[0] as! TPumpSettingsDatum).associations) - XCTAssertNotNil((created[1] as! TPumpSettingsOverrideDeviceEventDatum).duration) - XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations!.count, 1) - XCTAssertEqual((created[1] as! TPumpSettingsOverrideDeviceEventDatum).associations![0].id, created[0].id) - XCTAssertNotNil((created[2] as! TPumpSettingsOverrideDeviceEventDatum).duration) - XCTAssertEqual((created[2] as! TPumpSettingsOverrideDeviceEventDatum).associations!.count, 1) - XCTAssertEqual((created[2] as! TPumpSettingsOverrideDeviceEventDatum).associations![0].id, created[0].id) - XCTAssertEqual((created[3] as! TControllerSettingsDatum).device!.name, "Controller #1") - XCTAssertTrue(updated.isEmpty) - XCTAssertEqual(lastControllerSettings!.id, created[3].id) - XCTAssertNil(lastCGMSettings) - XCTAssertEqual(lastPumpSettings!.id, created[0].id) - XCTAssertNil(lastPumpSettingsOverride) - } -} - -fileprivate extension TemporaryScheduleOverride { - init(duration: TimeInterval? = nil) { - self.init(context: .custom, - settings: TemporaryScheduleOverrideSettings(targetRange: nil, insulinNeedsScaleFactor: 1.2), - startDate: Date(), - duration: duration != nil ? .finite(duration!) : .indefinite, - enactTrigger: .local, - syncIdentifier: UUID()) - } } fileprivate extension StoredSettings.ControllerDevice { diff --git a/TidepoolServiceKitUI/Extensions/EnvironmentValues.swift b/TidepoolServiceKitUI/Extensions/EnvironmentValues.swift new file mode 100644 index 0000000..56b465b --- /dev/null +++ b/TidepoolServiceKitUI/Extensions/EnvironmentValues.swift @@ -0,0 +1,20 @@ +// +// EnvironmentValues.swift +// TidepoolService +// +// Created by Nathaniel Hamming on 2024-10-30. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import SwiftUI + +private struct AllowDebugFeaturesKey: EnvironmentKey { + static let defaultValue: Bool = false +} + +public extension EnvironmentValues { + var allowDebugFeatures: Bool { + get { self[AllowDebugFeaturesKey.self] } + set { self[AllowDebugFeaturesKey.self] = newValue } + } +} diff --git a/TidepoolServiceKitUI/Localizable.xcstrings b/TidepoolServiceKitUI/Localizable.xcstrings index 0c949ad..993b729 100644 --- a/TidepoolServiceKitUI/Localizable.xcstrings +++ b/TidepoolServiceKitUI/Localizable.xcstrings @@ -293,6 +293,9 @@ } } }, + "Continue" : { + "comment" : "Delete Tidepool service button title" + }, "Delete Service" : { "comment" : "Button title to delete a service\nDelete Tidepool service button title", "localizations" : { diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift index 3280516..3a61ac8 100644 --- a/TidepoolServiceKitUI/SettingsView.swift +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -11,7 +11,8 @@ import TidepoolKit import TidepoolServiceKit public struct SettingsView: View { - + @Environment(\.allowDebugFeatures) var allowDebugFeatures + @State private var isEnvironmentActionSheetPresented = false @State private var showingDeletionConfirmation = false @@ -25,12 +26,18 @@ public struct SettingsView: View { private let login: ((TEnvironment) async throws -> Void)? private let dismiss: (() -> Void)? + private let onboarding: Bool var isLoggedIn: Bool { - return service.session != nil + service.session != nil + } + + var canDeleteService: Bool { + guard !allowDebugFeatures else { return true } + return !service.isDependency } - public init(service: TidepoolService, login: ((TEnvironment) async throws -> Void)?, dismiss: (() -> Void)?) + public init(service: TidepoolService, login: ((TEnvironment) async throws -> Void)?, dismiss: (() -> Void)?, onboarding: Bool) { let tapi = service.tapi self.service = service @@ -38,6 +45,7 @@ public struct SettingsView: View { self._selectedEnvironment = State(initialValue: service.session?.environment ?? defaultEnvironment ?? TEnvironment.productionEnvironment) self.login = login self.dismiss = dismiss + self.onboarding = onboarding } public var body: some View { @@ -95,9 +103,11 @@ public struct SettingsView: View { .padding() } Spacer() - if isLoggedIn { + if isLoggedIn && !onboarding && canDeleteService { deleteServiceButton - } else { + } else if isLoggedIn && onboarding { + continueButton + } else if !isLoggedIn { loginButton } } @@ -177,6 +187,17 @@ public struct SettingsView: View { .disabled(isLoggingIn) } + private var continueButton: some View { + Button(action: { + dismiss?() + }) { + Text(LocalizedString("Continue", comment: "Delete Tidepool service button title")) + } + .buttonStyle(ActionButtonStyle(.primary)) + .disabled(isLoggingIn) + } + + private func loginButtonTapped() { guard !isLoggingIn else { return @@ -211,6 +232,6 @@ public struct SettingsView: View { struct SettingsView_Previews: PreviewProvider { @MainActor static var previews: some View { - SettingsView(service: TidepoolService(hostIdentifier: "Previews", hostVersion: "1.0"), login: nil, dismiss: nil) + SettingsView(service: TidepoolService(hostIdentifier: "Previews", hostVersion: "1.0"), login: nil, dismiss: nil, onboarding: false) } } diff --git a/TidepoolServiceKitUI/TidepoolService+UI.swift b/TidepoolServiceKitUI/TidepoolService+UI.swift index b71f023..39634d7 100644 --- a/TidepoolServiceKitUI/TidepoolService+UI.swift +++ b/TidepoolServiceKitUI/TidepoolService+UI.swift @@ -29,12 +29,12 @@ enum TidepoolServiceError: Error { case missingWindow } -extension TidepoolService: ServiceUI { +extension TidepoolService: @retroactive ServiceUI { public static var image: UIImage? { UIImage(frameworkImage: "Tidepool Logo") } - public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult { + public static func setupViewController(pluginHost: PluginHost, onboarding: Bool, allowDebugFeatures: Bool) -> SetupUIResult { let navController = ServiceNavigationController() navController.isNavigationBarHidden = true @@ -48,7 +48,7 @@ extension TidepoolService: ServiceUI { throw TidepoolServiceError.missingWindow } - let windowContextProvider = WindowContextProvider(window: window) + let windowContextProvider = await WindowContextProvider(window: window) let sessionProvider = await ASWebAuthenticationSessionProvider(contextProviding: windowContextProvider) let auth = OAuth2Authenticator(api: service.tapi, environment: environment, sessionProvider: sessionProvider) try await auth.login() @@ -58,16 +58,20 @@ extension TidepoolService: ServiceUI { Task { await navController.notifyComplete() } - }) + }, onboarding: onboarding).environment(\.allowDebugFeatures, allowDebugFeatures) let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false) } - + return .userInteractionRequired(navController) } - public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController { + public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost, allowDebugFeatures: Bool) -> SetupUIResult { + return setupViewController(pluginHost: pluginHost, onboarding: false, allowDebugFeatures: allowDebugFeatures) + } + + public func settingsViewController(colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> ServiceViewController { let navController = ServiceNavigationController() navController.isNavigationBarHidden = true @@ -79,7 +83,7 @@ extension TidepoolService: ServiceUI { throw TidepoolServiceError.missingWindow } - let windowContextProvider = WindowContextProvider(window: window) + let windowContextProvider = await WindowContextProvider(window: window) let sessionProvider = await ASWebAuthenticationSessionProvider(contextProviding: windowContextProvider) let auth = OAuth2Authenticator(api: self.tapi, environment: environment, sessionProvider: sessionProvider) try await auth.login() @@ -88,7 +92,7 @@ extension TidepoolService: ServiceUI { Task { await navController.notifyComplete() } - }) + }, onboarding: false).environment(\.allowDebugFeatures, allowDebugFeatures) let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false)