From f4f3c4d03d31957387033cc56d40c46d993168fa Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming <nate@hmtconsulting.org> Date: Mon, 25 Sep 2023 15:25:53 -0300 Subject: [PATCH 01/21] [COASTAL-1291] plugin identifier is no longer class property (#92) --- TidepoolServiceKit/TidepoolService.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 00332ab..61ce54c 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") @@ -66,7 +68,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { 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") public init(hostIdentifier: String, hostVersion: String) { From 9975401517880a1d95660b7bdaed2e12c83b314a Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Tue, 26 Sep 2023 11:16:08 -0500 Subject: [PATCH 02/21] Merge fixes --- TidepoolServiceKit/TidepoolService.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 805eaa1..ab60271 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -33,7 +33,9 @@ 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") @@ -66,7 +68,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { 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") public init(hostIdentifier: String, hostVersion: String) { From 6cf2c2202a88e694f6cd2e62a4789f67913a6138 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming <nate@hmtconsulting.org> Date: Tue, 10 Oct 2023 15:43:52 -0300 Subject: [PATCH 03/21] added testflight configuration (#94) --- TidepoolService.xcodeproj/project.pbxproj | 235 +++++++++++++++++++++- 1 file changed, 234 insertions(+), 1 deletion(-) diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index be8ee8e..82cf48e 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -1354,6 +1354,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 +1588,7 @@ isa = XCConfigurationList; buildConfigurations = ( A94AE4EA235A89B5005CA320 /* Debug */, + B4E7CFAD2AD03299009B4DF2 /* Testflight */, A94AE4EB235A89B5005CA320 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1370,6 +1598,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAACF122E7978800E76C9F /* Debug */, + B4E7CFA82AD03299009B4DF2 /* Testflight */, A9DAACF222E7978800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1379,6 +1608,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD1122E7987800E76C9F /* Debug */, + B4E7CFA92AD03299009B4DF2 /* Testflight */, A9DAAD1222E7987800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1388,6 +1618,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD1422E7987800E76C9F /* Debug */, + B4E7CFAA2AD03299009B4DF2 /* Testflight */, A9DAAD1522E7987800E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1397,6 +1628,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD2D22E7988900E76C9F /* Debug */, + B4E7CFAB2AD03299009B4DF2 /* Testflight */, A9DAAD2E22E7988900E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; @@ -1406,6 +1638,7 @@ isa = XCConfigurationList; buildConfigurations = ( A9DAAD3022E7988900E76C9F /* Debug */, + B4E7CFAC2AD03299009B4DF2 /* Testflight */, A9DAAD3122E7988900E76C9F /* Release */, ); defaultConfigurationIsVisible = 0; From 103fb13296ea113e799010f987ede54661194638 Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Tue, 24 Oct 2023 10:42:17 -0500 Subject: [PATCH 04/21] Update test for api change --- .../Extensions/StoredDosingDecisionTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift index 78ca2ed..082152a 100644 --- a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift @@ -354,7 +354,6 @@ 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")!, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 75.5)))), date: dateFormatter.date(from: "2020-05-14T22:38:16Z")!) From 9fd01cf9000fccab79f3374c508aafccd5d3ee55 Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Tue, 19 Dec 2023 08:49:40 -0600 Subject: [PATCH 05/21] Temporary preset activations moved out of LoopSettings. (#96) --- .../Extensions/StoredSettings.swift | 41 +------ TidepoolServiceKit/TidepoolService.swift | 116 +++++------------- .../Extensions/StoredSettingsTests.swift | 38 ------ .../TidepoolServiceTests.swift | 78 +----------- 4 files changed, 36 insertions(+), 237 deletions(-) diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift index e3993b6..9e212cb 100644 --- a/TidepoolServiceKit/Extensions/StoredSettings.swift +++ b/TidepoolServiceKit/Extensions/StoredSettings.swift @@ -21,8 +21,6 @@ import TidepoolKit - preMealTargetRange ClosedRange<HKQuantity>? TPumpSettingsDatum.bloodGlucoseTargetPreprandial - workoutTargetRange ClosedRange<HKQuantity>? 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. @@ -116,29 +113,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 } @@ -304,7 +278,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 } @@ -333,19 +307,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" } } diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index f7590b6..ba7ea50 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -63,8 +63,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { private var lastPumpSettingsDatum: TPumpSettingsDatum? - private var lastPumpSettingsOverrideDeviceEventDatum: TPumpSettingsOverrideDeviceEventDatum? - private var hostIdentifier: String? private var hostVersion: String? @@ -95,7 +93,6 @@ 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) @@ -118,7 +115,6 @@ 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 } @@ -280,7 +276,32 @@ extension TidepoolService: TLogging { extension TidepoolService: RemoteDataService { public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride], completion: @escaping (Result<Bool, Error>) -> Void) { - // TODO: Implement + // TODO: https://tidepool.atlassian.net/browse/LOOP-4769 + + // The following code is taken from previous upload code when override events where stored in settings + // To be implemented with + + // 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) + completion(.success(true)) } @@ -448,7 +469,7 @@ extension TidepoolService: RemoteDataService { return } - 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 { @@ -457,7 +478,6 @@ extension TidepoolService: RemoteDataService { self.lastControllerSettingsDatum = lastControllerSettingsDatum self.lastCGMSettingsDatum = lastCGMSettingsDatum self.lastPumpSettingsDatum = lastPumpSettingsDatum - self.lastPumpSettingsOverrideDeviceEventDatum = lastPumpSettingsOverrideDeviceEventDatum self.completeUpdate() completion(.success(createdUploaded || updatedUploaded)) } catch { @@ -466,19 +486,12 @@ extension TidepoolService: RemoteDataService { } } - 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] = [] 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 { @@ -493,15 +506,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") @@ -517,13 +526,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 @@ -541,27 +548,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 { @@ -782,53 +771,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { } } -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/StoredSettingsTests.swift b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift index c08ce84..8e33ddf 100644 --- a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift @@ -247,42 +247,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 +356,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 { From 5d582adebd688cffc6fceca58a87fa44592df5b4 Mon Sep 17 00:00:00 2001 From: Cameron Ingham <camji555@gmail.com> Date: Tue, 16 Jan 2024 13:53:12 -0800 Subject: [PATCH 06/21] [LOOP-4788] Fix Unit Tests for iOS 17 --- .../Extensions/StoredDosingDecisionTests.swift | 2 +- .../Extensions/StoredGlucoseSampleTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift index 082152a..a0e7695 100644 --- a/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift +++ b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift @@ -138,7 +138,7 @@ class StoredDosingDecisionTests: XCTestCase { "amount" : 1.25 }, "requestedBolus" : { - "amount" : 0.80000000000000004 + "amount" : 0.8 }, "smbg" : { "time" : "2020-05-14T22:09:00.000Z", 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 From 2b8dc515f7dd4946c60384dcc9529effc78b4e4d Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Tue, 5 Mar 2024 12:06:27 -0600 Subject: [PATCH 07/21] LOOP-4781 Types moved to LoopAlgorithm (#98) * Types moved to LoopAlgorithm * Types moved to LoopAlgorithm --- TidepoolServiceKit/Extensions/InsulinType.swift | 1 + .../Extensions/StoredDosingDecisionTests.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift b/TidepoolServiceKitTests/Extensions/StoredDosingDecisionTests.swift index a0e7695..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 { @@ -354,7 +355,7 @@ fileprivate extension StoredDosingDecision { duration: .minutes(30)) let automaticDoseRecommendation = AutomaticDoseRecommendation(basalAdjustment: tempBasalRecommendation, bolusUnits: 1.25) let manualBolusRecommendation = ManualBolusRecommendationWithDate(recommendation: ManualBolusRecommendation(amount: 1.2, - 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 From bbc2c544c06d085f809665b14addd12cc8bf5e02 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming <nate@hmtconsulting.org> Date: Fri, 7 Jun 2024 14:15:39 -0300 Subject: [PATCH 08/21] [LOOP-4801] adding pump inoperable (#101) --- TidepoolServiceKit/Extensions/StoredDosingDecision.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 } } } From a723ade8980438bae8193d005a91844e58d59f1f Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Mon, 10 Jun 2024 10:15:55 -0500 Subject: [PATCH 09/21] LOOP-1169 - Upload device logs (#100) * Update for remote data service protocol changes * Upload device logs --- TidepoolService.xcodeproj/project.pbxproj | 4 + TidepoolServiceKit/DeviceLogUploader.swift | 130 +++++++++++++++++++ TidepoolServiceKit/TidepoolService.swift | 139 +++++++-------------- 3 files changed, 179 insertions(+), 94 deletions(-) create mode 100644 TidepoolServiceKit/DeviceLogUploader.swift diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index 82cf48e..a9a0191 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 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, ); }; }; + 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 */; }; @@ -231,6 +232,7 @@ C199E4DA29C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; C1A3529629C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; }; C1A3529729C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; }; + C1A685422C067E410071C171 /* DeviceLogUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLogUploader.swift; sourceTree = "<group>"; }; C1B0CFE129C786BF0045B04D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; C1B267AA2995824000BCB7C1 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; C1C9414529F0CB21008D3E05 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; }; @@ -402,6 +404,7 @@ A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */, A913B37B24200C86000805C4 /* Extensions */, A9DAAD4122E7DF9B00E76C9F /* Localizable.strings */, + C1A685422C067E410071C171 /* DeviceLogUploader.swift */, ); path = TidepoolServiceKit; sourceTree = "<group>"; @@ -752,6 +755,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 */, diff --git a/TidepoolServiceKit/DeviceLogUploader.swift b/TidepoolServiceKit/DeviceLogUploader.swift new file mode 100644 index 0000000..61b5a18 --- /dev/null +++ b/TidepoolServiceKit/DeviceLogUploader.swift @@ -0,0 +1,130 @@ +// +// 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) + + func setDelegate(_ delegate: RemoteDataServiceDelegate?) { + self.delegate = delegate + } + + init(api: TAPI) { + self.api = api + + Task { + await main() + } + } + + func main() async { + let backfillLimitInterval = TimeInterval(days: 2) + // Default start uploading logs from 2 days ago + var nextUploadStart = Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration) + + // Fetch device log metadata records + while true { + do { + // TODO: fetching logs is not implemented on the backend yet: awaiting https://tidepool.atlassian.net/browse/BACK-3011 + // For now, we expect this to error, so the catch has been modified to break out of the loop. Once this is implemented, + // We will want to retry on error, so the break should eventually be removed. + + 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 { + nextUploadStart = lastEnd.dateFlooredToTimeInterval(logChunkDuration) + } + break + } catch { + log.error("Unable to fetch device log metadata: %@", String(describing: error)) + try? await Task.sleep(nanoseconds: TimeInterval(minutes: 1).nanoseconds) + break // TODO: Remove when backend has implemented device log metadata fetching (see above) + } + } + // Start upload loop + while true { + let nextUploadEnd = nextUploadStart.addingTimeInterval(logChunkDuration) + let timeUntilNextUpload = nextUploadEnd.timeIntervalSinceNow + if timeUntilNextUpload > 0 { + log.debug("Waiting %@s until next upload", String(timeUntilNextUpload)) + try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds) + } + await upload(from: nextUploadStart, to: nextUploadEnd) + nextUploadStart = nextUploadEnd + } + } + + func upload(from start: Date, to end: Date) async { + log.default("Uploading from %@ to %@", String(describing: start), String(describing: end)) + do { + if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) { + log.default("Fetched %d logs", logs.count) + 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 + ) + }) + do { + let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end) + log.default("metadata: %@", String(describing: metatdata)) + print("hi") + } catch { + log.error("error uploading device logs:: %@", String(describing: error)) + print("hi") + } + } + } + } catch { + log.error("Upload failed: %@", String(describing: error)) + } + } +} + +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/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index ba7ea50..58d42e5 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -48,6 +48,8 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { public weak var stateDelegate: StatefulPluggableDelegate? + public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate? + public lazy var sessionStorage: SessionStorage = KeychainManager() public let tapi: TAPI = TAPI(clientId: BuildDetails.default.tidepoolServiceClientId, redirectURL: BuildDetails.default.tidepoolServiceRedirectURL) @@ -69,17 +71,25 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { private let log = OSLog(category: "TidepoolService") private let tidepoolKitLog = OSLog(category: "TidepoolKit") + private var deviceLogUploader: DeviceLogUploader? + 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 deviceLogUploader?.setDelegate(remoteDataServiceDelegate) + } + public init?(rawState: RawStateValue) { self.isOnboarded = true // Assume when restoring from state, that we're onboarded guard let id = rawState["id"] as? String else { @@ -96,8 +106,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { 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) @@ -275,7 +284,7 @@ extension TidepoolService: TLogging { extension TidepoolService: RemoteDataService { - public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride], completion: @escaping (Result<Bool, Error>) -> Void) { + public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride]) async throws { // TODO: https://tidepool.atlassian.net/browse/LOOP-4769 // The following code is taken from previous upload code when override events where stored in settings @@ -301,82 +310,49 @@ extension TidepoolService: RemoteDataService { // timeZoneOffset: datumTimeZoneOffset, // payload: datumPayload, // origin: origin) - - completion(.success(true)) } public var alertDataLimit: Int? { return 1000 } - public func uploadAlertData(_ stored: [SyncAlertObject], completion: @escaping (_ result: Result<Bool, Error>) -> 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<Bool, Error>) -> 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<Bool, Error>) -> Void) { + 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)) - } - } + let _ = try await createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) + let _ = try await deleteData(withSelectors: deleted.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<Bool, Error>) -> 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] { @@ -427,63 +403,39 @@ extension TidepoolService: RemoteDataService { public var glucoseDataLimit: Int? { return 1000 } - public func uploadGlucoseData(_ stored: [StoredGlucoseSample], completion: @escaping (Result<Bool, Error>) -> 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<Bool, Error>) -> 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<Bool, Error>) -> 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) = 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.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?) { @@ -606,9 +558,8 @@ extension TidepoolService: RemoteDataService { } } - public func uploadCgmEventData(_ stored: [LoopKit.PersistedCgmEvent], completion: @escaping (Result<Bool, Error>) -> Void) { + public func uploadCgmEventData(_ stored: [PersistedCgmEvent]) async throws { // TODO: Upload sensor/transmitter changes - completion(.success(false)) } public func remoteNotificationWasReceived(_ notification: [String: AnyObject]) async throws { From 0b9dbdb331cc69adb9042487435d6e8a04ba08f7 Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Tue, 27 Aug 2024 11:51:15 -0500 Subject: [PATCH 10/21] Upload temporary presets (#103) --- TidepoolService.xcodeproj/project.pbxproj | 4 + .../Extensions/StoredSettings.swift | 74 ----------- .../TemporaryScheduleOverride.swift | 120 ++++++++++++++++++ TidepoolServiceKit/TidepoolService.swift | 33 +---- 4 files changed, 131 insertions(+), 100 deletions(-) create mode 100644 TidepoolServiceKit/Extensions/TemporaryScheduleOverride.swift diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index a9a0191..76a25dd 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 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 */; }; @@ -226,6 +227,7 @@ C110888E2A39149100BA4898 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; }; C12522E1298309B5006EA1CD /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; }; C1317D4129830A0800625B94 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; + C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryScheduleOverride.swift; sourceTree = "<group>"; }; C18B726B299581C600F138D3 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; }; C192C60B29C78711001EFEA6 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; }; C199E4D929C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; @@ -322,6 +324,7 @@ A97A60FA243818C900AD69A5 /* TDatum.swift */, A9752A9C270B972D00E50750 /* TimeInterval.swift */, A9D10DB727AB2CCF00814B7B /* SyncAlertObject.swift */, + C14559A52C7CF49100541EF1 /* TemporaryScheduleOverride.swift */, ); path = Extensions; sourceTree = "<group>"; @@ -766,6 +769,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 */, diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift index 9e212cb..4b9a84f 100644 --- a/TidepoolServiceKit/Extensions/StoredSettings.swift +++ b/TidepoolServiceKit/Extensions/StoredSettings.swift @@ -390,80 +390,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 58d42e5..b1a1b1f 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -285,31 +285,12 @@ extension TidepoolService: TLogging { extension TidepoolService: RemoteDataService { public func uploadTemporaryOverrideData(updated: [TemporaryScheduleOverride], deleted: [TemporaryScheduleOverride]) async throws { - // TODO: https://tidepool.atlassian.net/browse/LOOP-4769 - - // The following code is taken from previous upload code when override events where stored in settings - // To be implemented with - - // 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) + 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 } @@ -440,7 +421,7 @@ extension TidepoolService: RemoteDataService { 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 From 5caeee373a8cddac4324145d4cc3dd13b9edafcd Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Fri, 13 Sep 2024 16:10:37 -0500 Subject: [PATCH 11/21] LOOP-4098 - Specify correct duration for basal segments (#104) * Specify correct duration for basal segments * Fix test --- TidepoolServiceKit/Extensions/DoseEntry.swift | 4 ++-- TidepoolServiceKitTests/Extensions/DoseEntryTests.swift | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift index c9f7c85..73b1461 100644 --- a/TidepoolServiceKit/Extensions/DoseEntry.swift +++ b/TidepoolServiceKit/Extensions/DoseEntry.swift @@ -82,8 +82,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) diff --git a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift index 56d6a9c..5027623 100644 --- a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift +++ b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift @@ -38,7 +38,6 @@ class DoseEntryDataTests: XCTestCase { { "deliveryType" : "automated", "duration" : 1500000, - "expectedDuration" : 1800000, "id" : "f839af02f6832d7c81d636dbbbadbc01", "insulinFormulation" : { "simple" : { From e192e327530e598c8acffabf14af970001b5a333 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming <nate@hmtconsulting.org> Date: Wed, 18 Sep 2024 16:21:18 -0300 Subject: [PATCH 12/21] [LOOP-5035] report time zone changes (#105) * report time zone changes * removed unneeded print statement * set device log uploader when remoteDataServiceDelegate is set --- TidepoolServiceKit/DeviceLogUploader.swift | 2 - TidepoolServiceKit/TidepoolService.swift | 44 ++++++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/TidepoolServiceKit/DeviceLogUploader.swift b/TidepoolServiceKit/DeviceLogUploader.swift index 61b5a18..4ed7344 100644 --- a/TidepoolServiceKit/DeviceLogUploader.swift +++ b/TidepoolServiceKit/DeviceLogUploader.swift @@ -91,10 +91,8 @@ actor DeviceLogUploader { do { let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end) log.default("metadata: %@", String(describing: metatdata)) - print("hi") } catch { log.error("error uploading device logs:: %@", String(describing: error)) - print("hi") } } } diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index b1a1b1f..3089277 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -48,8 +48,14 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { public weak var stateDelegate: StatefulPluggableDelegate? - public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate? - + 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) @@ -72,6 +78,10 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { private let tidepoolKitLog = OSLog(category: "TidepoolKit") private var deviceLogUploader: DeviceLogUploader? + + private func setDeviceLogUploaderDelegate() async { + await deviceLogUploader?.setDelegate(remoteDataServiceDelegate) + } public init(hostIdentifier: String, hostVersion: String) { self.id = UUID().uuidString @@ -87,7 +97,8 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { await tapi.setLogging(self) await tapi.addObserver(self) deviceLogUploader = DeviceLogUploader(api: tapi) - await deviceLogUploader?.setDelegate(remoteDataServiceDelegate) + await setDeviceLogUploaderDelegate() + observeTimeZoneChanges() } public init?(rawState: RawStateValue) { @@ -485,6 +496,33 @@ extension TidepoolService: RemoteDataService { return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum) } + + private func uploadTimeZoneChangeData(from fromTimeZone: TimeZone, to toTimeZone: TimeZone, method: TTimeChangeDeviceEventDatum.Method = .automatic, at date: Date = Date()) async throws { + guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { + throw TidepoolServiceError.configuration + } + + let timeZoneChangeData = TTimeChangeDeviceEventDatum(time: date, + from: TTimeChangeDeviceEventDatum.Info(timeZoneName: fromTimeZone.identifier), + to: TTimeChangeDeviceEventDatum.Info(timeZoneName: toTimeZone.identifier), + method: method) + let _ = try await createData([timeZoneChangeData]) + } + + private func observeTimeZoneChanges() { + NotificationCenter.default.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: .main) { notification in + if let previousTimeZone = notification.object as? TimeZone { + let currentTimeZone = TimeZone.current + Task { + do { + try await self.uploadTimeZoneChangeData(from: previousTimeZone, to: currentTimeZone) + } catch { + self.log.error("Failed to upload time zone change data - %{public}@", error.localizedDescription) + } + } + } + } + } private func createData(_ data: [TDatum]) async throws -> Bool { if let error = error { From b98c3113fda650d04537cef2ecca566fa6d61d88 Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Wed, 18 Sep 2024 14:30:14 -0500 Subject: [PATCH 13/21] LOOP-5065 Device log upload fixes (#106) * Device log upload fixes * Fix comment to match code --- TidepoolServiceKit/DeviceLogUploader.swift | 113 +++++++++++---------- 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/TidepoolServiceKit/DeviceLogUploader.swift b/TidepoolServiceKit/DeviceLogUploader.swift index 4ed7344..b27b524 100644 --- a/TidepoolServiceKit/DeviceLogUploader.swift +++ b/TidepoolServiceKit/DeviceLogUploader.swift @@ -21,6 +21,9 @@ actor DeviceLogUploader { private var logChunkDuration = TimeInterval(hours: 1) + private let backfillLimitInterval = TimeInterval(days: 2) + + func setDelegate(_ delegate: RemoteDataServiceDelegate?) { self.delegate = delegate } @@ -34,70 +37,72 @@ actor DeviceLogUploader { } func main() async { - let backfillLimitInterval = TimeInterval(days: 2) - // Default start uploading logs from 2 days ago - var nextUploadStart = Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration) + var nextLogStart: Date? - // Fetch device log metadata records + // Start upload loop while true { - do { - // TODO: fetching logs is not implemented on the backend yet: awaiting https://tidepool.atlassian.net/browse/BACK-3011 - // For now, we expect this to error, so the catch has been modified to break out of the loop. Once this is implemented, - // We will want to retry on error, so the break should eventually be removed. - - var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date()) - uploadMetadata.sort { a, b in - return a.endAtTime > b.endAtTime + if nextLogStart == nil { + do { + nextLogStart = try await getMostRecentUploadEndTime() + } catch { + log.error("Unable to fetch device log metadata: %{public}@", String(describing: error)) } - if let lastEnd = uploadMetadata.last?.endAtTime { - nextUploadStart = lastEnd.dateFlooredToTimeInterval(logChunkDuration) + } + + 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) } - break - } catch { - log.error("Unable to fetch device log metadata: %@", String(describing: error)) - try? await Task.sleep(nanoseconds: TimeInterval(minutes: 1).nanoseconds) - break // TODO: Remove when backend has implemented device log metadata fetching (see above) + } 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) } } - // Start upload loop - while true { - let nextUploadEnd = nextUploadStart.addingTimeInterval(logChunkDuration) - let timeUntilNextUpload = nextUploadEnd.timeIntervalSinceNow - if timeUntilNextUpload > 0 { - log.debug("Waiting %@s until next upload", String(timeUntilNextUpload)) - try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds) - } - await upload(from: nextUploadStart, to: nextUploadEnd) - nextUploadStart = nextUploadEnd + } + + 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 { - log.default("Uploading from %@ to %@", String(describing: start), String(describing: end)) - do { - if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) { - log.default("Fetched %d logs", logs.count) - 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 - ) - }) - do { - let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end) - log.default("metadata: %@", String(describing: metatdata)) - } catch { - log.error("error uploading device logs:: %@", String(describing: error)) - } - } + 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)) } - } catch { - log.error("Upload failed: %@", String(describing: error)) } } } From 78ed7b7a6b83f734cd3d46aa55d9254ea62215f1 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming <nate@hmtconsulting.org> Date: Mon, 23 Sep 2024 13:38:39 -0300 Subject: [PATCH 14/21] [LOOP-5035] Pump event time zone sync (#107) * reverting code * uploading pump time zone sync event * response to comments --- .../Extensions/PersistedPumpEvent.swift | 33 +++++++++++++++++++ TidepoolServiceKit/TidepoolService.swift | 31 +---------------- 2 files changed, 34 insertions(+), 30 deletions(-) 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/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 3089277..e8a5719 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -64,7 +64,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { private let id: String - private var lastControllerSettingsDatum: TControllerSettingsDatum? private var lastCGMSettingsDatum: TCGMSettingsDatum? @@ -78,7 +77,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { private let tidepoolKitLog = OSLog(category: "TidepoolKit") private var deviceLogUploader: DeviceLogUploader? - + private func setDeviceLogUploaderDelegate() async { await deviceLogUploader?.setDelegate(remoteDataServiceDelegate) } @@ -98,7 +97,6 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { await tapi.addObserver(self) deviceLogUploader = DeviceLogUploader(api: tapi) await setDeviceLogUploaderDelegate() - observeTimeZoneChanges() } public init?(rawState: RawStateValue) { @@ -496,33 +494,6 @@ extension TidepoolService: RemoteDataService { return (created, updated, lastControllerSettingsDatum, lastCGMSettingsDatum, lastPumpSettingsDatum) } - - private func uploadTimeZoneChangeData(from fromTimeZone: TimeZone, to toTimeZone: TimeZone, method: TTimeChangeDeviceEventDatum.Method = .automatic, at date: Date = Date()) async throws { - guard let userId = userId, let hostIdentifier = hostIdentifier, let hostVersion = hostVersion else { - throw TidepoolServiceError.configuration - } - - let timeZoneChangeData = TTimeChangeDeviceEventDatum(time: date, - from: TTimeChangeDeviceEventDatum.Info(timeZoneName: fromTimeZone.identifier), - to: TTimeChangeDeviceEventDatum.Info(timeZoneName: toTimeZone.identifier), - method: method) - let _ = try await createData([timeZoneChangeData]) - } - - private func observeTimeZoneChanges() { - NotificationCenter.default.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: .main) { notification in - if let previousTimeZone = notification.object as? TimeZone { - let currentTimeZone = TimeZone.current - Task { - do { - try await self.uploadTimeZoneChangeData(from: previousTimeZone, to: currentTimeZone) - } catch { - self.log.error("Failed to upload time zone change data - %{public}@", error.localizedDescription) - } - } - } - } - } private func createData(_ data: [TDatum]) async throws -> Bool { if let error = error { From 54f12b50223150178142141a0494a50b574af274 Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Wed, 2 Oct 2024 17:18:39 -0500 Subject: [PATCH 15/21] LOOP-5071 Overlay basal and automation history (#108) * Overlay basal and automation history * Test updates * Annotate suspends with scheduled basal --- TidepoolServiceKit/Extensions/DoseEntry.swift | 193 +++++++++++++++++- TidepoolServiceKit/TidepoolService.swift | 29 ++- .../Extensions/DoseEntryTests.swift | 133 +++++++++++- 3 files changed, 350 insertions(+), 5 deletions(-) diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift index 73b1461..257d6eb 100644 --- a/TidepoolServiceKit/Extensions/DoseEntry.swift +++ b/TidepoolServiceKit/Extensions/DoseEntry.swift @@ -8,6 +8,8 @@ import LoopKit import TidepoolKit +import LoopAlgorithm +import HealthKit /* DoseEntry @@ -204,8 +206,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) @@ -342,3 +344,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<Double>]) -> [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<Double>]) -> [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<Bool>] + ) -> [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/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index e8a5719..c7fb62e 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -326,13 +326,38 @@ extension TidepoolService: RemoteDataService { public var doseDataLimit: Int? { return 1000 } + 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 { throw TidepoolServiceError.configuration } - let _ = try await createData(created.flatMap { $0.data(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) }) - let _ = try await deleteData(withSelectors: deleted.flatMap { $0.selectors }) + // 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 diff --git a/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift b/TidepoolServiceKitTests/Extensions/DoseEntryTests.swift index 5027623..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 { @@ -549,7 +550,8 @@ class DoseEntryDataTests: XCTestCase { } ], "deliveryType" : "automated", - "duration" : 0, + "duration" : 1200000, + "expectedDuration" : 1800000, "id" : "f839af02f6832d7c81d636dbbbadbc01", "insulinFormulation" : { "simple" : { @@ -720,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<Bool>] = [ + 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<Bool>] = [ + 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<Bool>] = [ + 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<Bool>] = [ + 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<Bool>] = [ + 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<Bool>] = [ + 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<Bool>] = [ + 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() } From e7ad5fe15a1757248f93bc266dd45d2fc2d2e7ef Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Fri, 25 Oct 2024 14:38:49 -0500 Subject: [PATCH 16/21] Use continue button during onboarding (#110) --- TidepoolServiceKitUI/SettingsView.swift | 21 ++++++++++++++++--- TidepoolServiceKitUI/TidepoolService+UI.swift | 18 +++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/TidepoolServiceKitUI/SettingsView.swift b/TidepoolServiceKitUI/SettingsView.swift index 3280516..bacffd3 100644 --- a/TidepoolServiceKitUI/SettingsView.swift +++ b/TidepoolServiceKitUI/SettingsView.swift @@ -25,12 +25,13 @@ 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 } - 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 +39,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,8 +97,10 @@ public struct SettingsView: View { .padding() } Spacer() - if isLoggedIn { + if isLoggedIn && !onboarding { deleteServiceButton + } else if isLoggedIn { + continueButton } else { loginButton } @@ -177,6 +181,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 +226,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..06e3d2c 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<ServiceViewController, ServiceUI> { + public static func setupViewController(pluginHost: PluginHost, onboarding: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> { 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,15 +58,19 @@ extension TidepoolService: ServiceUI { Task { await navController.notifyComplete() } - }) + }, onboarding: onboarding) let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false) } - + return .userInteractionRequired(navController) } + public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult<ServiceViewController, ServiceUI> { + return setupViewController(pluginHost: pluginHost, onboarding: false) + } + public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController { let navController = ServiceNavigationController() @@ -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) let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false) From 65e5c778e16bf583303dddba9bf010fbae48988d Mon Sep 17 00:00:00 2001 From: Cameron Ingham <camji555@gmail.com> Date: Tue, 29 Oct 2024 11:37:33 -0700 Subject: [PATCH 17/21] [LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSettingsDatum and TCGMSettingsDatum --- TidepoolServiceKit/TidepoolService.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index c7fb62e..699f06b 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -468,9 +468,12 @@ extension TidepoolService: RemoteDataService { let controllerSettingsDatumIsEffectivelyEquivalent = TControllerSettingsDatum.areEffectivelyEquivalent(old: lastControllerSettingsDatum, new: controllerSettingsDatum) let cgmSettingsDatum = $0.datumCGMSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + cgmSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 let cgmSettingsDatumIsEffectivelyEquivalent = TCGMSettingsDatum.areEffectivelyEquivalent(old: lastCGMSettingsDatum, new: cgmSettingsDatum) let pumpSettingsDatum = $0.datumPumpSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) + pumpSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 + pumpSettingsDatum.automatedDelivery = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 let pumpSettingsDatumIsEffectivelyEquivalent = TPumpSettingsDatum.areEffectivelyEquivalent(old: lastPumpSettingsDatum, new: pumpSettingsDatum) // Associate the data @@ -640,7 +643,7 @@ extension TCGMSettingsDatum: EffectivelyEquivalent { self.manufacturers == other.manufacturers && self.model == other.model && self.name == other.name && - self.serialNumber == other.serialNumber && +// self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 self.softwareVersion == other.softwareVersion && self.transmitterId == other.transmitterId && self.units == other.units && @@ -676,7 +679,7 @@ 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.automatedDelivery == other.automatedDelivery && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 self.basal == other.basal && self.basalRateSchedule == other.basalRateSchedule && self.basalRateSchedules == other.basalRateSchedules && @@ -700,7 +703,7 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { self.name == other.name && self.overridePresets == other.overridePresets && self.scheduleTimeZoneOffset == other.scheduleTimeZoneOffset && - self.serialNumber == other.serialNumber && +// self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 self.softwareVersion == other.softwareVersion && self.units == other.units } From 98545f004b978457f539d66b2b83af56b266b12c Mon Sep 17 00:00:00 2001 From: Cameron Ingham <camji555@gmail.com> Date: Tue, 29 Oct 2024 12:47:38 -0700 Subject: [PATCH 18/21] [LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSettingsDatum and TCGMSettingsDatum --- TidepoolServiceKit/Extensions/StoredSettings.swift | 9 --------- TidepoolServiceKit/TidepoolService.swift | 9 --------- 2 files changed, 18 deletions(-) diff --git a/TidepoolServiceKit/Extensions/StoredSettings.swift b/TidepoolServiceKit/Extensions/StoredSettings.swift index 4b9a84f..2051827 100644 --- a/TidepoolServiceKit/Extensions/StoredSettings.swift +++ b/TidepoolServiceKit/Extensions/StoredSettings.swift @@ -65,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, @@ -82,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, @@ -102,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) @@ -145,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 } @@ -155,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 @@ -293,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 { diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 699f06b..5514e85 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -468,12 +468,9 @@ extension TidepoolService: RemoteDataService { let controllerSettingsDatumIsEffectivelyEquivalent = TControllerSettingsDatum.areEffectivelyEquivalent(old: lastControllerSettingsDatum, new: controllerSettingsDatum) let cgmSettingsDatum = $0.datumCGMSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) - cgmSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 let cgmSettingsDatumIsEffectivelyEquivalent = TCGMSettingsDatum.areEffectivelyEquivalent(old: lastCGMSettingsDatum, new: cgmSettingsDatum) let pumpSettingsDatum = $0.datumPumpSettings(for: userId, hostIdentifier: hostIdentifier, hostVersion: hostVersion) - pumpSettingsDatum.serialNumber = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 - pumpSettingsDatum.automatedDelivery = nil // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 let pumpSettingsDatumIsEffectivelyEquivalent = TPumpSettingsDatum.areEffectivelyEquivalent(old: lastPumpSettingsDatum, new: pumpSettingsDatum) // Associate the data @@ -643,7 +640,6 @@ extension TCGMSettingsDatum: EffectivelyEquivalent { self.manufacturers == other.manufacturers && self.model == other.model && self.name == other.name && -// self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 self.softwareVersion == other.softwareVersion && self.transmitterId == other.transmitterId && self.units == other.units && @@ -662,7 +658,6 @@ extension TCGMSettingsDatum: EffectivelyEquivalent { manufacturers == nil && model == nil && name == nil && - serialNumber == nil && softwareVersion == nil && transmitterId == nil && defaultAlerts == nil && @@ -679,7 +674,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 && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 self.basal == other.basal && self.basalRateSchedule == other.basalRateSchedule && self.basalRateSchedules == other.basalRateSchedules && @@ -703,7 +697,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { self.name == other.name && self.overridePresets == other.overridePresets && self.scheduleTimeZoneOffset == other.scheduleTimeZoneOffset && -// self.serialNumber == other.serialNumber && // MARK: Planned for removal in https://tidepool.atlassian.net/browse/LOOP-5130 self.softwareVersion == other.softwareVersion && self.units == other.units } @@ -711,7 +704,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 && @@ -735,7 +727,6 @@ extension TPumpSettingsDatum: EffectivelyEquivalent { name == nil && overridePresets == nil && scheduleTimeZoneOffset == nil && - serialNumber == nil && softwareVersion == nil } } From 8c7256364eb32e9ec9627baffa8093d9309aae2c Mon Sep 17 00:00:00 2001 From: Cameron Ingham <camji555@gmail.com> Date: Tue, 29 Oct 2024 12:57:57 -0700 Subject: [PATCH 19/21] [LOOP-5132] Remove automatedDelivery and serialNumber from TPumpSettingsDatum and TCGMSettingsDatum --- TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift b/TidepoolServiceKitTests/Extensions/StoredSettingsTests.swift index 8e33ddf..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", From 342c8e9e07e90559a84722daaa114fd3aee26595 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming <nate@hmtconsulting.org> Date: Wed, 30 Oct 2024 16:31:41 -0300 Subject: [PATCH 20/21] [PAL-818] plugin dependency (#112) * do not allow deleting of the service when it is a dependency * pass allowDebugFeatures to service --- TidepoolService.xcodeproj/project.pbxproj | 4 ++++ TidepoolServiceKit/TidepoolService.swift | 8 +++++++- .../Extensions/EnvironmentValues.swift | 20 +++++++++++++++++++ TidepoolServiceKitUI/SettingsView.swift | 16 ++++++++++----- TidepoolServiceKitUI/TidepoolService+UI.swift | 12 +++++------ 5 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 TidepoolServiceKitUI/Extensions/EnvironmentValues.swift diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index 76a25dd..ed7e8ac 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -63,6 +63,7 @@ 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; }; @@ -224,6 +225,7 @@ A9E8C610272C76A500016E2E /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = "<group>"; }; A9F9F316271A046E00D19374 /* StoredCarbEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCarbEntry.swift; sourceTree = "<group>"; }; A9F9F318271A05B100D19374 /* IdentifiableHKDatum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableHKDatum.swift; sourceTree = "<group>"; }; + B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; }; C110888E2A39149100BA4898 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; }; C12522E1298309B5006EA1CD /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; }; C1317D4129830A0800625B94 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; @@ -482,6 +484,7 @@ C1D0B62A29848BD90098D215 /* Extensions */ = { isa = PBXGroup; children = ( + B40B20CB2CD2AC600027BF35 /* EnvironmentValues.swift */, C1D0B62B29848BEB0098D215 /* Image.swift */, C1C9414529F0CB21008D3E05 /* UIImage.swift */, ); @@ -806,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; diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 5514e85..7da1509 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -60,7 +60,7 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { 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 @@ -77,6 +77,8 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { private let tidepoolKitLog = OSLog(category: "TidepoolKit") private var deviceLogUploader: DeviceLogUploader? + + public var isDependency: Bool = false private func setDeviceLogUploaderDelegate() async { await deviceLogUploader?.setDelegate(remoteDataServiceDelegate) @@ -137,6 +139,10 @@ public final class TidepoolService: Service, TAPIObserver, ObservableObject { } public var isOnboarded = false // No distinction between created and onboarded + + public func markAsDepedency(_ isDependency: Bool) { + self.isDependency = isDependency + } @Published public var session: TSession? 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 bacffd3..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 @@ -28,7 +29,12 @@ public struct SettingsView: View { 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)?, onboarding: Bool) @@ -97,11 +103,11 @@ public struct SettingsView: View { .padding() } Spacer() - if isLoggedIn && !onboarding { + if isLoggedIn && !onboarding && canDeleteService { deleteServiceButton - } else if isLoggedIn { + } else if isLoggedIn && onboarding { continueButton - } else { + } else if !isLoggedIn { loginButton } } diff --git a/TidepoolServiceKitUI/TidepoolService+UI.swift b/TidepoolServiceKitUI/TidepoolService+UI.swift index 06e3d2c..39634d7 100644 --- a/TidepoolServiceKitUI/TidepoolService+UI.swift +++ b/TidepoolServiceKitUI/TidepoolService+UI.swift @@ -34,7 +34,7 @@ extension TidepoolService: @retroactive ServiceUI { UIImage(frameworkImage: "Tidepool Logo") } - public static func setupViewController(pluginHost: PluginHost, onboarding: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> { + public static func setupViewController(pluginHost: PluginHost, onboarding: Bool, allowDebugFeatures: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> { let navController = ServiceNavigationController() navController.isNavigationBarHidden = true @@ -58,7 +58,7 @@ extension TidepoolService: @retroactive ServiceUI { Task { await navController.notifyComplete() } - }, onboarding: onboarding) + }, onboarding: onboarding).environment(\.allowDebugFeatures, allowDebugFeatures) let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false) @@ -67,11 +67,11 @@ extension TidepoolService: @retroactive ServiceUI { return .userInteractionRequired(navController) } - public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult<ServiceViewController, ServiceUI> { - return setupViewController(pluginHost: pluginHost, onboarding: false) + public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost, allowDebugFeatures: Bool) -> SetupUIResult<ServiceViewController, ServiceUI> { + return setupViewController(pluginHost: pluginHost, onboarding: false, allowDebugFeatures: allowDebugFeatures) } - public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController { + public func settingsViewController(colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> ServiceViewController { let navController = ServiceNavigationController() navController.isNavigationBarHidden = true @@ -92,7 +92,7 @@ extension TidepoolService: @retroactive ServiceUI { Task { await navController.notifyComplete() } - }, onboarding: false) + }, onboarding: false).environment(\.allowDebugFeatures, allowDebugFeatures) let hostingController = await UIHostingController(rootView: settingsView) await navController.pushViewController(hostingController, animated: false) From 2cd32ca193eedd4fb4ed769679f36f1a01126669 Mon Sep 17 00:00:00 2001 From: Pete Schwamb <pete@schwamb.net> Date: Thu, 31 Oct 2024 11:08:40 -0500 Subject: [PATCH 21/21] Fix deletion bug with manual injection bolus --- TidepoolServiceKit/Extensions/DoseEntry.swift | 8 +++++--- TidepoolServiceKit/TidepoolService.swift | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/TidepoolServiceKit/Extensions/DoseEntry.swift b/TidepoolServiceKit/Extensions/DoseEntry.swift index 9448fe2..e8e9b21 100644 --- a/TidepoolServiceKit/Extensions/DoseEntry.swift +++ b/TidepoolServiceKit/Extensions/DoseEntry.swift @@ -301,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)] diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 7da1509..f41ebdf 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -357,6 +357,10 @@ extension TidepoolService: RemoteDataService { throw TidepoolServiceError.configuration } + 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) })