diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index 40b91ca..ed7e8ac 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -63,12 +63,15 @@ A9E8C611272C76A500016E2E /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8C610272C76A500016E2E /* TimeInterval.swift */; }; A9F9F317271A046E00D19374 /* StoredCarbEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F9F316271A046E00D19374 /* StoredCarbEntry.swift */; }; A9F9F319271A05B100D19374 /* IdentifiableHKDatum.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */; }; + 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 */; }; @@ -222,15 +225,18 @@ A9E8C610272C76A500016E2E /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; A9F9F316271A046E00D19374 /* StoredCarbEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCarbEntry.swift; sourceTree = ""; }; A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableHKDatum.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 = ""; }; C12522E1298309B5006EA1CD /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; C1317D4129830A0800625B94 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryScheduleOverride.swift; sourceTree = ""; }; C18B726B299581C600F138D3 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; C192C60B29C78711001EFEA6 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; C199E4D929C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; C199E4DA29C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; C1A3529629C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; C1A3529729C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; + C1A685422C067E410071C171 /* DeviceLogUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLogUploader.swift; sourceTree = ""; }; C1B0CFE129C786BF0045B04D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; C1B267AA2995824000BCB7C1 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; C1C9414529F0CB21008D3E05 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; @@ -320,6 +326,7 @@ A97A60FA243818C900AD69A5 /* TDatum.swift */, A9752A9C270B972D00E50750 /* TimeInterval.swift */, A9D10DB727AB2CCF00814B7B /* SyncAlertObject.swift */, + C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */, ); path = Extensions; sourceTree = ""; @@ -402,6 +409,7 @@ A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */, A913B37B24200C86000805C4 /* Extensions */, A9DAAD4122E7DF9B00E76C9F /* Localizable.strings */, + C1A685422C067E410071C171 /* DeviceLogUploader.swift */, ); path = TidepoolServiceKit; sourceTree = ""; @@ -476,6 +484,7 @@ C1D0B62A29848BD90098D215 /* Extensions */ = { isa = PBXGroup; children = ( + B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */, C1D0B62B29848BEB0098D215 /* Image.swift */, C1C9414529F0CB21008D3E05 /* UIImage.swift */, ); @@ -752,6 +761,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 */, @@ -762,6 +772,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 */, @@ -798,6 +809,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; @@ -1354,6 +1366,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; + 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 = 15.1; + 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 */ @@ -1361,6 +1600,7 @@ isa = XCConfigurationList; buildConfigurations = ( A94AE4EA235A89B5005CA320 /* Debug */, + B4E7CFAD2AD03299009B4DF2 /* Testflight */, A94AE4EB235A89B5005CA320 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1370,6 +1610,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAACF122E7978800E76C9F /* Debug */, + B4E7CFA82AD03299009B4DF2 /* Testflight */, A9DAACF222E7978800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1379,6 +1620,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD1122E7987800E76C9F /* Debug */, + B4E7CFA92AD03299009B4DF2 /* Testflight */, A9DAAD1222E7987800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1388,6 +1630,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD1422E7987800E76C9F /* Debug */, + B4E7CFAA2AD03299009B4DF2 /* Testflight */, A9DAAD1522E7987800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1397,6 +1640,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD2D22E7988900E76C9F /* Debug */, + B4E7CFAB2AD03299009B4DF2 /* Testflight */, A9DAAD2E22E7988900E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1406,6 +1650,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD3022E7988900E76C9F /* Debug */, + B4E7CFAC2AD03299009B4DF2 /* Testflight */, A9DAAD3122E7988900E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/TidepoolServiceKit/DeviceLogUploader.swift b/TidepoolServiceKit/DeviceLogUploader.swift new file mode 100644 index 0000000..b27b524 --- /dev/null +++ b/TidepoolServiceKit/DeviceLogUploader.swift @@ -0,0 +1,133 @@ +// +// DeviceLogUploader.swift +// TidepoolServiceKit +// +// Created by Pete Schwamb on 5/28/24. +// Copyright © 2024 LoopKit Authors. All rights reserved. +// + +import Foundation +import os.log +import LoopKit +import TidepoolKit + +/// Periodically uploads device logs in hourly chunks to backend +actor DeviceLogUploader { + private let log = OSLog(category: "DeviceLogUploader") + + private let api: TAPI + + private var delegate: RemoteDataServiceDelegate? + + private var logChunkDuration = TimeInterval(hours: 1) + + private let backfillLimitInterval = TimeInterval(days: 2) + + + func setDelegate(_ delegate: RemoteDataServiceDelegate?) { + self.delegate = delegate + } + + init(api: TAPI) { + self.api = api + + Task { + await main() + } + } + + func main() async { + var nextLogStart: Date? + + // Start upload loop + while true { + if nextLogStart == nil { + do { + nextLogStart = try await getMostRecentUploadEndTime() + } catch { + log.error("Unable to fetch device log metadata: %{public}@", String(describing: error)) + } + } + + if nextLogStart != nil { + let nextLogEnd = nextLogStart!.addingTimeInterval(logChunkDuration) + let timeUntilNextUpload = nextLogEnd.timeIntervalSinceNow + if timeUntilNextUpload > 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..e8e9b21 100644 --- a/TidepoolServiceKit/Extensions/DoseEntry.swift +++ b/TidepoolServiceKit/Extensions/DoseEntry.swift @@ -8,6 +8,8 @@ import LoopKit import TidepoolKit +import LoopAlgorithm +import HealthKit /* DoseEntry @@ -82,8 +84,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) @@ -209,8 +211,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) @@ -299,9 +301,11 @@ extension DoseEntry { case .basal: return [datumSelector(for: TScheduledBasalDatum.self)] case .bolus: - if manuallyEntered { - return [datumSelector(for: TInsulinDatum.self)] - } else if automatic != true { + // TODO: revert to using .insulin datum type once fully supported in Tidepool frontend +// if manuallyEntered { +// return [datumSelector(for: TInsulinDatum.self)] +// } else if automatic != true { + if automatic != true { return [datumSelector(for: TNormalBolusDatum.self)] } else { return [datumSelector(for: TAutomatedBolusDatum.self)] @@ -347,3 +351,190 @@ 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 = HKQuantity(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 = HKQuantity(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 { + 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/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/StoredDosingDecision.swift b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift index 7710caf..24e3f56 100644 --- a/TidepoolServiceKit/Extensions/StoredDosingDecision.swift +++ b/TidepoolServiceKit/Extensions/StoredDosingDecision.swift @@ -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 } } } diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift index e3993b6..2051827 100644 --- a/TidepoolServiceKit/Extensions/StoredSettings.swift +++ b/TidepoolServiceKit/Extensions/StoredSettings.swift @@ -21,8 +21,6 @@ import TidepoolKit - 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 +36,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 +65,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,7 +81,6 @@ 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, @@ -105,7 +100,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 +110,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 +142,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 +150,6 @@ extension StoredSettings: IdentifiableDatum { return Self.activeScheduleNameDefault } - private var datumPumpAutomatedDelivery: Bool { dosingEnabled } - private var datumPumpBasal: TPumpSettingsDatum.Basal? { guard let maximumBasalRatePerHour = maximumBasalRatePerHour else { return nil @@ -304,7 +271,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 +286,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 +298,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" } } @@ -429,80 +381,6 @@ fileprivate extension TemporaryScheduleOverridePreset { 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 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) - } -} - extension TCGMSettingsDatum: TypedDatum { static var resolvedType: String { TDatum.DatumType.cgmSettings.rawValue } } diff --git a/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift b/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift new file mode 100644 index 0000000..2bcc203 --- /dev/null +++ b/TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift @@ -0,0 +1,120 @@ +// +// 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 .legacyWorkout: + return .physicalActivity + case .preset(_): + return .preset + case .custom: + return .custom + } + } +} + +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) + } +} + +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 + } + 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..f41ebdf 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,11 +135,14 @@ 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? @@ -277,83 +299,85 @@ 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) { + private func annotateDoses(_ doses: [DoseEntry]) async throws -> [DoseEntry] { + guard !doses.isEmpty else { + return [] + } + + 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 { - completion(.failure(TidepoolServiceError.configuration)) - return + throw TidepoolServiceError.configuration } - 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 !created.isEmpty || !deleted.isEmpty else { + return } + + // 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 +428,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 +483,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 +503,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 +525,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 +583,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 +650,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 +668,6 @@ extension TCGMSettingsDatum: EffectivelyEquivalent { manufacturers == nil && model == nil && name == nil && - serialNumber == nil && softwareVersion == nil && transmitterId == nil && defaultAlerts == nil && @@ -719,7 +684,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 +707,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 +714,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 +737,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..06a2a10 100644 --- a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift +++ b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift @@ -13,6 +13,7 @@ import Foundation import HealthKit import LoopKit import TidepoolKit +import LoopAlgorithm @testable import TidepoolServiceKit class DoseEntryDataTests: XCTestCase { @@ -38,7 +39,6 @@ class DoseEntryDataTests: XCTestCase { { "deliveryType" : "automated", "duration" : 1500000, - "expectedDuration" : 1800000, "id" : "f839af02f6832d7c81d636dbbbadbc01", "insulinFormulation" : { "simple" : { @@ -550,7 +550,8 @@ class DoseEntryDataTests: XCTestCase { } ], "deliveryType" : "automated", - "duration" : 0, + "duration" : 1200000, + "expectedDuration" : 1800000, "id" : "f839af02f6832d7c81d636dbbbadbc01", "insulinFormulation" : { "simple" : { @@ -721,5 +722,134 @@ class DoseEntrySelectorTests: XCTestCase { 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, 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, 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, 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, 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, automatic: nil, manuallyEntered: false, isMutable: false), + DoseEntry(type: .basal, startDate: now.addingTimeInterval(300), endDate: now.addingTimeInterval(3600), value: 1.0, unit: .unitsPerHour, 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, 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, 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, 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/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift index 78ca2ed..668c1cf 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", @@ -354,8 +355,7 @@ fileprivate extension StoredDosingDecision { duration: .minutes(30)) let automaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: tempBasalRecommendation, 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")!, + notice: .predictedGlucoseBelowTarget(minGlucose: SimpleGlucoseValue(startDate: dateFormatter.date(from: "2020-05-14T23:03:15Z")!, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 75.5)))), date: dateFormatter.date(from: "2020-05-14T22:38:16Z")!) let manualBolusRequested = 0.8 diff --git a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift index 18641d5..ac1c165 100644 --- a/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredGlucoseSampleTests.swift @@ -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 diff --git a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift index c08ce84..0d985eb 100644 --- a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift @@ -78,7 +78,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 +94,6 @@ class StoredSettingsTests: XCTestCase { XCTAssertEqual(String(data: data, encoding: .utf8), """ { "activeSchedule" : "Default", - "automatedDelivery" : true, "basal" : { "rateMaximum" : { "units" : "Units/hour", @@ -231,7 +229,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 +244,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) @@ -392,8 +353,6 @@ fileprivate extension StoredSettings { preMealTargetRange: preMealTargetRange, workoutTargetRange: workoutTargetRange, overridePresets: overridePresets, - scheduleOverride: scheduleOverride, - preMealOverride: preMealOverride, maximumBasalRatePerHour: maximumBasalRatePerHour, maximumBolus: maximumBolus, suspendThreshold: suspendThreshold, 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/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)