From 34a825e70ff2e52c69e20c053417d178994370a8 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Mon, 25 Sep 2023 15:52:21 -0300 Subject: [PATCH 1/5] [COASTAL-1291] plugin identifier is no longer class property (#39) --- NightscoutServiceKit/NightscoutService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NightscoutServiceKit/NightscoutService.swift b/NightscoutServiceKit/NightscoutService.swift index 281a6db..762491e 100644 --- a/NightscoutServiceKit/NightscoutService.swift +++ b/NightscoutServiceKit/NightscoutService.swift @@ -20,7 +20,7 @@ public enum NightscoutServiceError: Error { public final class NightscoutService: Service { - public static let pluginIdentifier = "NightscoutService" + public let pluginIdentifier = "NightscoutService" public static let localizedTitle = LocalizedString("Nightscout", comment: "The title of the Nightscout service") From 8f2f54eb384cc134cb4d06f41859ec76e5337932 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 12 Dec 2023 12:37:37 -0600 Subject: [PATCH 2/5] Updates for LOOP-4752 --- NightscoutServiceKit/Extensions/StoredSettings.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NightscoutServiceKit/Extensions/StoredSettings.swift b/NightscoutServiceKit/Extensions/StoredSettings.swift index 0bed9c6..2b2aff8 100644 --- a/NightscoutServiceKit/Extensions/StoredSettings.swift +++ b/NightscoutServiceKit/Extensions/StoredSettings.swift @@ -49,8 +49,8 @@ extension StoredSettings { return NightscoutKit.LoopSettings( dosingEnabled: dosingEnabled, - overridePresets: overridePresets?.map { $0.nsScheduleOverride(for: bloodGlucoseUnit) } ?? [], - scheduleOverride: scheduleOverride?.nsScheduleOverride(for: bloodGlucoseUnit), + overridePresets: overridePresets.map { $0.nsScheduleOverride(for: bloodGlucoseUnit) }, + scheduleOverride: nil, minimumBGGuard: suspendThreshold?.quantity.doubleValue(for: bloodGlucoseUnit), preMealTargetRange: nightscoutPreMealTargetRange, maximumBasalRatePerHour: maximumBasalRatePerHour, From 1e1b51910e52826eb2a15e649d05b15e2aec1e82 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 23 May 2024 17:30:38 -0500 Subject: [PATCH 3/5] Update for remote data service protocol changes --- NightscoutServiceKit/NightscoutService.swift | 243 ++++++++++--------- 1 file changed, 135 insertions(+), 108 deletions(-) diff --git a/NightscoutServiceKit/NightscoutService.swift b/NightscoutServiceKit/NightscoutService.swift index b5ec9c4..d0563b1 100644 --- a/NightscoutServiceKit/NightscoutService.swift +++ b/NightscoutServiceKit/NightscoutService.swift @@ -153,10 +153,8 @@ public final class NightscoutService: Service { } extension NightscoutService: RemoteDataService { - - public func uploadTemporaryOverrideData(updated: [LoopKit.TemporaryScheduleOverride], deleted: [LoopKit.TemporaryScheduleOverride], completion: @escaping (Result) -> Void) { + public func uploadTemporaryOverrideData(updated: [LoopKit.TemporaryScheduleOverride], deleted: [LoopKit.TemporaryScheduleOverride]) async throws { guard let uploader = uploader else { - completion(.success(true)) return } @@ -164,118 +162,120 @@ extension NightscoutService: RemoteDataService { let deletions = deleted.map { $0.syncIdentifier.uuidString } - uploader.deleteTreatmentsById(deletions, completionHandler: { (error) in - if let error = error { - self.log.error("Overrides deletions failed to delete %{public}@: %{public}@", String(describing: deletions), String(describing: error)) - } else { - if deletions.count > 0 { - self.log.debug("Deleted ids: %@", deletions) - } - uploader.upload(updates) { (result) in - switch result { - case .failure(let error): - self.log.error("Failed to upload overrides %{public}@: %{public}@", String(describing: updates.map {$0.dictionaryRepresentation}), String(describing: error)) - completion(.failure(error)) - case .success: - self.log.debug("Uploaded overrides %@", String(describing: updates.map {$0.dictionaryRepresentation})) - completion(.success(true)) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.deleteTreatmentsById(deletions, completionHandler: { (error) in + if let error = error { + self.log.error("Overrides deletions failed to delete %{public}@: %{public}@", String(describing: deletions), String(describing: error)) + } else { + if deletions.count > 0 { + self.log.debug("Deleted ids: %@", deletions) + } + uploader.upload(updates) { (result) in + switch result { + case .failure(let error): + self.log.error("Failed to upload overrides %{public}@: %{public}@", String(describing: updates.map {$0.dictionaryRepresentation}), String(describing: error)) + continuation.resume(throwing: error) + case .success: + self.log.debug("Uploaded overrides %@", String(describing: updates.map {$0.dictionaryRepresentation})) + continuation.resume() + } } } - } + }) }) } public var alertDataLimit: Int? { return 1000 } - public func uploadAlertData(_ stored: [SyncAlertObject], completion: @escaping (Result) -> Void) { - completion(.success(false)) + public func uploadAlertData(_ stored: [SyncAlertObject]) async throws { } public var carbDataLimit: Int? { return 1000 } - public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject], completion: @escaping (Result) -> Void) { + public func uploadCarbData(created: [SyncCarbObject], updated: [SyncCarbObject], deleted: [SyncCarbObject]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.createCarbData(created) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let createdObjectIds): - let createdUploaded = !created.isEmpty - let syncIdentifiers = created.map { $0.syncIdentifier } - for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { - if let syncIdentifier = syncIdentifier { - self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.createCarbData(created) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let createdObjectIds): + let createdUploaded = !created.isEmpty + let syncIdentifiers = created.map { $0.syncIdentifier } + for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { + if let syncIdentifier = syncIdentifier { + self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + } } - } - self.stateDelegate?.pluginDidUpdateState(self) - - uploader.updateCarbData(updated, usingObjectIdCache: self.objectIdCache) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let updatedUploaded): - uploader.deleteCarbData(deleted, usingObjectIdCache: self.objectIdCache) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let deletedUploaded): - self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) - self.stateDelegate?.pluginDidUpdateState(self) - completion(.success(createdUploaded || updatedUploaded || deletedUploaded)) + self.stateDelegate?.pluginDidUpdateState(self) + + uploader.updateCarbData(updated, usingObjectIdCache: self.objectIdCache) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let updatedUploaded): + uploader.deleteCarbData(deleted, usingObjectIdCache: self.objectIdCache) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let deletedUploaded): + self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) + self.stateDelegate?.pluginDidUpdateState(self) + continuation.resume() + } } } } } } - } + }) } public var doseDataLimit: Int? { return 1000 } - public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry], completion: @escaping (_ result: Result) -> Void) { + public func uploadDoseData(created: [DoseEntry], deleted: [DoseEntry]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.createDoses(created, usingObjectIdCache: self.objectIdCache) { (result) in - switch (result) { - case .failure(let error): - completion(.failure(error)) - case .success(let createdObjectIds): - let createdUploaded = !created.isEmpty - let syncIdentifiers = created.map { $0.syncIdentifier } - for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { - if let syncIdentifier = syncIdentifier { - self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.createDoses(created, usingObjectIdCache: self.objectIdCache) { (result) in + switch (result) { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let createdObjectIds): + let createdUploaded = !created.isEmpty + let syncIdentifiers = created.map { $0.syncIdentifier } + for (syncIdentifier, objectId) in zip(syncIdentifiers, createdObjectIds) { + if let syncIdentifier = syncIdentifier { + self.objectIdCache.add(syncIdentifier: syncIdentifier, objectId: objectId) + } } - } - self.stateDelegate?.pluginDidUpdateState(self) - - uploader.deleteDoses(deleted.filter { !$0.isMutable }, usingObjectIdCache: self.objectIdCache) { result in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let deletedUploaded): - self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) - self.stateDelegate?.pluginDidUpdateState(self) - completion(.success(createdUploaded || deletedUploaded)) + self.stateDelegate?.pluginDidUpdateState(self) + + uploader.deleteDoses(deleted.filter { !$0.isMutable }, usingObjectIdCache: self.objectIdCache) { result in + switch result { + case .failure(let error): + continuation.resume(throwing: error) + case .success(let deletedUploaded): + self.objectIdCache.purge(before: Date().addingTimeInterval(-self.objectIdCacheKeepTime)) + self.stateDelegate?.pluginDidUpdateState(self) + continuation.resume() + } } } } - } + }) } 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) -> Void) { + public func uploadDosingDecisionData(_ stored: [StoredDosingDecision]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } @@ -297,42 +297,50 @@ extension NightscoutService: RemoteDataService { } guard statuses.count > 0 else { - completion(.success(false)) return } - uploader.uploadDeviceStatuses(statuses) { result in - switch result { - case .success: - self.lastDosingDecisionForAutomaticDose = nil - default: - break + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadDeviceStatuses(statuses) { result in + switch result { + case .success: + self.lastDosingDecisionForAutomaticDose = nil + default: + break + } + continuation.resume() } - completion(result) - } + }) } public var glucoseDataLimit: Int? { return 1000 } - public func uploadGlucoseData(_ stored: [StoredGlucoseSample], completion: @escaping (Result) -> Void) { + public func uploadGlucoseData(_ stored: [StoredGlucoseSample]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.uploadGlucoseSamples(stored, completion: completion) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadGlucoseSamples(stored) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + }) } public var pumpEventDataLimit: Int? { return 1000 } - public func uploadPumpEventData(_ stored: [PersistedPumpEvent], completion: @escaping (Result) -> Void) { + public func uploadPumpEventData(_ stored: [PersistedPumpEvent]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - let source = "loop://\(UIDevice.current.name)" + let source = "loop://\(await UIDevice.current.name)" let treatments = stored.compactMap { (event) -> NightscoutTreatment? in // ignore doses; we'll get those via uploadDoseData @@ -342,29 +350,37 @@ extension NightscoutService: RemoteDataService { return event.treatment(source: source) } - uploader.upload(treatments) { (result) in - switch result { - case .failure(let error): - self.log.error("Failed to upload pump events %{public}@: %{public}@", String(describing: treatments.map {$0.dictionaryRepresentation}), String(describing: error)) - completion(.failure(error)) - case .success: - self.log.debug("Uploaded overrides %@", String(describing: treatments.map {$0.dictionaryRepresentation})) - completion(.success(true)) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.upload(treatments) { (result) in + switch result { + case .failure(let error): + self.log.error("Failed to upload pump events %{public}@: %{public}@", String(describing: treatments.map {$0.dictionaryRepresentation}), String(describing: error)) + continuation.resume(throwing: error) + case .success: + self.log.debug("Uploaded overrides %@", String(describing: treatments.map {$0.dictionaryRepresentation})) + continuation.resume() + } } - } - - completion(.success(false)) + }) } 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) -> Void) { + public func uploadSettingsData(_ stored: [StoredSettings]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.uploadProfiles(stored.compactMap { $0.profileSet }, completion: completion) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadProfiles(stored.compactMap { $0.profileSet }) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + }) } public func fetchStoredTherapySettings(completion: @escaping (Result<(TherapySettings,Date), Error>) -> Void) { @@ -388,13 +404,21 @@ extension NightscoutService: RemoteDataService { }) } - public func uploadCgmEventData(_ stored: [LoopKit.PersistedCgmEvent], completion: @escaping (Result) -> Void) { + public func uploadCgmEventData(_ stored: [LoopKit.PersistedCgmEvent]) async throws { guard hasConfiguration, let uploader = uploader else { - completion(.success(true)) return } - uploader.uploadCgmEvents(stored, completion: completion) + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) -> Void in + uploader.uploadCgmEvents(stored) { result in + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } + } + }) } @@ -407,6 +431,9 @@ extension NightscoutService: RemoteDataService { return commandSourceV1 } + public func uploadDeviceLogs(_ stored: [StoredDeviceLogEntry], startTime: Date, endTime: Date) async throws { + // TODO + } } extension NightscoutService: RemoteCommandSourceV1Delegate { From b50a1c3da0b511b88e0cb566d715f9de8cdf5943 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 3 Jun 2024 20:48:00 -0500 Subject: [PATCH 4/5] Remote data service can fetch device logs --- NightscoutServiceKit/NightscoutService.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/NightscoutServiceKit/NightscoutService.swift b/NightscoutServiceKit/NightscoutService.swift index d0563b1..de38a2a 100644 --- a/NightscoutServiceKit/NightscoutService.swift +++ b/NightscoutServiceKit/NightscoutService.swift @@ -30,6 +30,8 @@ public final class NightscoutService: Service { public weak var stateDelegate: StatefulPluggableDelegate? + public weak var remoteDataServiceDelegate: RemoteDataServiceDelegate? + public var siteURL: URL? public var apiSecret: String? @@ -431,9 +433,6 @@ extension NightscoutService: RemoteDataService { return commandSourceV1 } - public func uploadDeviceLogs(_ stored: [StoredDeviceLogEntry], startTime: Date, endTime: Date) async throws { - // TODO - } } extension NightscoutService: RemoteCommandSourceV1Delegate { From a70df077eaff5b714c839dcd8ce6313ad76c095d Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Wed, 30 Oct 2024 16:34:52 -0300 Subject: [PATCH 5/5] service allowDebugFeatures (#42) --- NightscoutServiceKitUI/NightscoutService+UI.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NightscoutServiceKitUI/NightscoutService+UI.swift b/NightscoutServiceKitUI/NightscoutService+UI.swift index 94a6921..26d3894 100644 --- a/NightscoutServiceKitUI/NightscoutService+UI.swift +++ b/NightscoutServiceKitUI/NightscoutService+UI.swift @@ -16,11 +16,11 @@ extension NightscoutService: ServiceUI { UIImage(named: "nightscout", in: Bundle(for: ServiceUICoordinator.self), compatibleWith: nil)! } - public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost) -> SetupUIResult { + public static func setupViewController(colorPalette: LoopUIColorPalette, pluginHost: PluginHost, allowDebugFeatures: Bool) -> SetupUIResult { return .userInteractionRequired(ServiceUICoordinator(colorPalette: colorPalette)) } - public func settingsViewController(colorPalette: LoopUIColorPalette) -> ServiceViewController { + public func settingsViewController(colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> ServiceViewController { return ServiceUICoordinator(service: self, colorPalette: colorPalette) } }