Skip to content

Commit 99519a7

Browse files
committed
Remote Action Models
1 parent f6efd72 commit 99519a7

File tree

6 files changed

+195
-311
lines changed

6 files changed

+195
-311
lines changed

Loop/Managers/DeviceDataManager.swift

Lines changed: 156 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,18 @@ extension DeviceDataManager {
812812
self.loopManager.updateRemoteRecommendation()
813813
}
814814
}
815+
816+
func enactBolus(units: Double, activationType: BolusActivationType) async throws {
817+
return try await withCheckedThrowingContinuation { continuation in
818+
enactBolus(units: units, activationType: activationType) { error in
819+
if let error = error {
820+
continuation.resume(throwing: error)
821+
return
822+
}
823+
continuation.resume()
824+
}
825+
}
826+
}
815827

816828
var pumpManagerStatus: PumpManagerStatus? {
817829
return pumpManager?.status
@@ -1341,8 +1353,15 @@ extension Notification.Name {
13411353

13421354
// MARK: - Remote Notification Handling
13431355
extension DeviceDataManager {
1356+
13441357
func handleRemoteNotification(_ notification: [String: AnyObject]) {
1345-
1358+
Task {
1359+
await handleRemoteNotification(notification)
1360+
}
1361+
}
1362+
1363+
func handleRemoteNotification(_ notification: [String: AnyObject]) async {
1364+
13461365
defer {
13471366
log.default("Remote Notification: Finished handling")
13481367
}
@@ -1351,9 +1370,6 @@ extension DeviceDataManager {
13511370
log.error("Remote Notification: Overrides not enabled.")
13521371
return
13531372
}
1354-
1355-
let defaultServiceIdentifier = "NightscoutService"
1356-
let serviceIdentifer = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier
13571373

13581374
if let expirationStr = notification["expiration"] as? String {
13591375
let formatter = ISO8601DateFormatter()
@@ -1362,7 +1378,7 @@ extension DeviceDataManager {
13621378
let nowDate = Date()
13631379
guard nowDate < expiration else {
13641380
let expiredInterval = nowDate.timeIntervalSince(expiration)
1365-
NotificationManager.sendRemoteCommandExpiredNotification(timeExpired: expiredInterval)
1381+
await NotificationManager.sendRemoteCommandExpiredNotification(timeExpired: expiredInterval)
13661382
log.error("Remote Notification: Expired: %{public}@", String(describing: notification))
13671383
return
13681384
}
@@ -1372,156 +1388,175 @@ extension DeviceDataManager {
13721388
}
13731389
}
13741390

1375-
let command: RemoteCommand
1391+
let action: RemoteAction
13761392

13771393
do {
1378-
command = try RemoteCommand.createRemoteCommand(notification: notification, allowedPresets: loopManager.settings.overridePresets, defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium).get()
1394+
action = try RemoteAction.createRemoteAction(notification: notification).get()
13791395
} catch {
13801396
log.error("Remote Notification: Parse Error: %{public}@", String(describing: error))
13811397
return
13821398
}
1383-
1384-
switch command {
1385-
1386-
case .temporaryScheduleOverride(let remoteOverride):
1387-
log.default("Remote Notification: Enacting temporary override: %{public}@", String(describing: remoteOverride))
1388-
activateRemoteOverride(remoteOverride)
1389-
case .cancelTemporaryOverride:
1390-
log.default("Remote Notification: Canceling temporary override")
1391-
activateRemoteOverride(nil)
1392-
case .bolusEntry(let bolusAmount):
1393-
log.default("Remote Notification: Enacting bolus entry: %{public}@", String(describing: bolusAmount))
1394-
1395-
//Remote bolus requires validation from its remote source
1396-
1397-
let validationResult = remoteDataServicesManager.validatePushNotificationSource(notification, serviceIdentifier: serviceIdentifer)
1398-
1399-
switch validationResult {
1400-
case .success():
1401-
log.info("Remote Notification: Validation successful")
1402-
case .failure(let error):
1403-
NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAmount)
1404-
log.error("Remote Notification: Could not validate notification: %{public}@", String(describing: notification))
1405-
return
1406-
}
1407-
1408-
guard let maxBolusAmount = loopManager.settings.maximumBolus else {
1409-
NotificationManager.sendRemoteBolusFailureNotification(for: RemoteCommandError.missingMaxBolus, amount: bolusAmount)
1410-
log.error("Remote Notification: No max bolus detected. Aborting...")
1411-
return
1412-
}
1413-
1414-
guard bolusAmount.isLessThanOrEqualTo(maxBolusAmount) else {
1415-
NotificationManager.sendRemoteBolusFailureNotification(for: RemoteCommandError.exceedsMaxBolus, amount: bolusAmount)
1416-
log.error("Remote Notification: Bolus exceeds maximum allowed. Aborting...")
1417-
return
1418-
}
1419-
1420-
self.enactRemoteBolus(units: bolusAmount) { error in
1421-
if let error = error {
1422-
self.log.error("Remote Notification: Error adding bolus: %{public}@", String(describing: error))
1423-
NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAmount)
1424-
} else {
1425-
NotificationManager.sendRemoteBolusNotification(amount: bolusAmount)
1426-
}
1427-
}
1428-
case .carbsEntry(let candidateCarbEntry):
1429-
log.default("Remote Notification: Adding carbs entry: %{public}@", String(describing: candidateCarbEntry))
1430-
1431-
let candidateCarbsInGrams = candidateCarbEntry.quantity.doubleValue(for: .gram())
1432-
1433-
//Remote carb entry requires validation from its remote source
1434-
let validationResult = remoteDataServicesManager.validatePushNotificationSource(notification, serviceIdentifier: serviceIdentifer)
1435-
switch validationResult {
1436-
case .success():
1437-
log.info("Remote Notification: Validation successful")
1438-
case .failure(let error):
1439-
NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: candidateCarbsInGrams)
1440-
log.error("Remote Notification: Could not validate notification: %{public}@", String(describing: notification))
1441-
return
1399+
1400+
log.default("Remote Notification: Handling action %{public}@", String(describing: action))
1401+
1402+
switch action {
1403+
case .temporaryScheduleOverride(let overrideAction):
1404+
do {
1405+
try await handleRemoteOverrideAction(overrideAction)
1406+
} catch {
1407+
log.error("Remote Notification: Override Action Error: %{public}@", String(describing: error))
14421408
}
1443-
1444-
guard candidateCarbsInGrams > 0.0 else {
1445-
NotificationManager.sendRemoteCarbEntryFailureNotification(for: RemoteCommandError.invalidCarbs, amountInGrams: candidateCarbsInGrams)
1446-
log.error("Remote Notification: Invalid carb entry amount. Aborting...")
1447-
return
1409+
case .cancelTemporaryOverride(let overrideCancelAction):
1410+
do {
1411+
try await handleRemoteOverrideCancelAction(overrideCancelAction)
1412+
} catch {
1413+
log.error("Remote Notification: Override Action Cancel Error: %{public}@", String(describing: error))
14481414
}
1449-
1450-
guard candidateCarbsInGrams <= LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()) else {
1451-
NotificationManager.sendRemoteCarbEntryFailureNotification(for: RemoteCommandError.exceedsMaxCarbs, amountInGrams: candidateCarbsInGrams)
1452-
log.error("Remote Notification: Carbs higher than maximum. Aborting...")
1453-
return
1415+
case .bolusEntry(let bolusAction):
1416+
do {
1417+
try validatePushNotificationSource(notification: notification)
1418+
try await handleRemoteBolusAction(bolusAction)
1419+
} catch {
1420+
await NotificationManager.sendRemoteBolusFailureNotification(for: error, amount: bolusAction.amountInUnits)
1421+
log.error("Remote Notification: Bolus Action Error: %{public}@", String(describing: notification))
14541422
}
1455-
1456-
addRemoteCarbEntry(candidateCarbEntry) { carbEntryAddResult in
1457-
switch carbEntryAddResult {
1458-
case .success(let completedCarbEntry):
1459-
NotificationManager.sendRemoteCarbEntryNotification(amountInGrams: completedCarbEntry.quantity.doubleValue(for: .gram()))
1460-
case .failure(let error):
1461-
self.log.error("Remote Notification: Error adding carb entry: %{public}@", String(describing: error))
1462-
NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: candidateCarbsInGrams)
1463-
}
1423+
case .carbsEntry(let carbAction):
1424+
do {
1425+
try validatePushNotificationSource(notification: notification)
1426+
try await handleRemoteCarbAction(carbAction)
1427+
} catch {
1428+
await NotificationManager.sendRemoteCarbEntryFailureNotification(for: error, amountInGrams: carbAction.amountInGrams)
1429+
log.error("Remote Notification: Carb Action Error: %{public}@", String(describing: notification))
14641430
}
14651431
}
14661432
}
14671433

1468-
func activateRemoteOverride(_ remoteOverride: TemporaryScheduleOverride?) {
1434+
func validatePushNotificationSource(notification: [String: AnyObject]) throws {
1435+
1436+
let defaultServiceIdentifier = "NightscoutService"
1437+
let serviceIdentifer = notification["serviceIdentifier"] as? String ?? defaultServiceIdentifier
1438+
1439+
let validationResult = remoteDataServicesManager.validatePushNotificationSource(notification, serviceIdentifier: serviceIdentifer)
1440+
switch validationResult {
1441+
case .success():
1442+
log.info("Remote Notification: Validation successful")
1443+
case .failure(let error):
1444+
throw error
1445+
}
1446+
}
1447+
1448+
//Remote Overrides
1449+
1450+
func handleRemoteOverrideAction(_ action: RemoteOverrideAction) async throws {
1451+
let remoteOverride = try action.toValidOverride(allowedPresets: loopManager.settings.overridePresets)
1452+
await activateRemoteOverride(remoteOverride)
1453+
}
1454+
1455+
func handleRemoteOverrideCancelAction(_ cancelAction: RemoteOverrideCancelAction) async throws {
1456+
await activateRemoteOverride(nil)
1457+
}
1458+
1459+
func activateRemoteOverride(_ remoteOverride: TemporaryScheduleOverride?) async {
14691460
loopManager.mutateSettings { settings in settings.scheduleOverride = remoteOverride }
1470-
self.triggerBackgroundUpload(for: .overrides)
1461+
await remoteDataServicesManager.triggerUpload(for: .overrides)
14711462
}
14721463

1473-
func addRemoteCarbEntry(_ carbEntry: NewCarbEntry, completion: @escaping (_ result: CarbStoreResult<StoredCarbEntry>) -> Void) {
1474-
1475-
var backgroundTask: UIBackgroundTaskIdentifier?
1476-
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Add Remote Carb Entry") {
1477-
guard let backgroundTask = backgroundTask else {return}
1478-
UIApplication.shared.endBackgroundTask(backgroundTask)
1479-
self.log.error("Add Remote Carb Entry background task expired")
1464+
//Remote Bolus
1465+
1466+
func handleRemoteBolusAction(_ bolusCommand: RemoteBolusAction) async throws {
1467+
let validBolusAmount = try bolusCommand.toValidBolusAmount(maximumBolus: loopManager.settings.maximumBolus)
1468+
try await self.enactBolus(units: validBolusAmount, activationType: .manualNoRecommendation)
1469+
await triggerBackgroundUpload(for: .dose)
1470+
self.analyticsServicesManager.didBolus(source: "Remote", units: validBolusAmount)
1471+
}
1472+
1473+
//Remote Carb Entry
1474+
1475+
func handleRemoteCarbAction(_ carbCommand: RemoteCarbAction) async throws {
1476+
let candidateCarbEntry = try carbCommand.toValidCarbEntry(
1477+
defaultAbsorptionTime: carbStore.defaultAbsorptionTimes.medium,
1478+
minAbsorptionTime: LoopConstants.minCarbAbsorptionTime,
1479+
maxAbsorptionTime: LoopConstants.maxCarbAbsorptionTime,
1480+
maxCarbEntryQuantity: LoopConstants.maxCarbEntryQuantity.doubleValue(for: .gram()),
1481+
maxCarbEntryPastTime: LoopConstants.maxCarbEntryPastTime,
1482+
maxCarbEntryFutureTime: LoopConstants.maxCarbEntryFutureTime
1483+
)
1484+
1485+
let _ = try await addRemoteCarbEntry(candidateCarbEntry)
1486+
await triggerBackgroundUpload(for: .carb)
1487+
}
1488+
1489+
//Can't add this concurrency wrapper method to Loopkit due to the minimum iOS version
1490+
func addRemoteCarbEntry(_ carbEntry: NewCarbEntry) async throws -> StoredCarbEntry {
1491+
return try await withCheckedThrowingContinuation { continuation in
1492+
carbStore.addCarbEntry(carbEntry) { result in
1493+
switch result {
1494+
case .success(let storedCarbEntry):
1495+
self.analyticsServicesManager.didAddCarbs(source: "Remote", amount: carbEntry.quantity.doubleValue(for: .gram()))
1496+
continuation.resume(returning: storedCarbEntry)
1497+
case .failure(let error):
1498+
continuation.resume(throwing: error)
1499+
}
1500+
}
14801501
}
1481-
1482-
carbStore.addCarbEntry(carbEntry) { result in
1483-
self.triggerBackgroundUpload(for: .carb)
1484-
if let backgroundTask = backgroundTask {
1485-
UIApplication.shared.endBackgroundTask(backgroundTask)
1502+
}
1503+
1504+
//Background Uploads
1505+
1506+
func triggerBackgroundUpload(for triggeringType: RemoteDataType) async {
1507+
Task {
1508+
return try await withCheckedThrowingContinuation { continuation in
1509+
triggerBackgroundUpload(for: triggeringType) {
1510+
continuation.resume()
1511+
}
14861512
}
1487-
completion(result)
1488-
self.analyticsServicesManager.didAddCarbs(source: "Remote", amount: carbEntry.quantity.doubleValue(for: .gram()))
14891513
}
14901514
}
14911515

1492-
func enactRemoteBolus(units: Double, completion: @escaping (_ error: Error?) -> Void = { _ in }) {
1516+
func triggerBackgroundUpload(for triggeringType: RemoteDataType, completion: (() -> Void)? = nil) {
14931517

14941518
var backgroundTask: UIBackgroundTaskIdentifier?
1495-
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Enact Remote Bolus") {
1496-
guard let backgroundTask = backgroundTask else {return}
1497-
UIApplication.shared.endBackgroundTask(backgroundTask)
1498-
self.log.error("Enact Remote Bolus background task expired")
1499-
}
1519+
backgroundTask = beginBackgroundTask(name: "Remote Data Upload")
15001520

1501-
self.enactBolus(units: units, activationType: .manualNoRecommendation) { error in
1502-
self.triggerBackgroundUpload(for: .dose)
1503-
if let backgroundTask = backgroundTask {
1504-
UIApplication.shared.endBackgroundTask(backgroundTask)
1505-
}
1506-
completion(error)
1507-
self.analyticsServicesManager.didBolus(source: "Remote", units: units)
1521+
self.remoteDataServicesManager.triggerUpload(for: triggeringType) {
1522+
self.endBackgroundTask(backgroundTask)
1523+
completion?()
15081524
}
15091525
}
15101526

1511-
func triggerBackgroundUpload(for triggeringType: RemoteDataType) {
1512-
1527+
func beginBackgroundTask(name: String) -> UIBackgroundTaskIdentifier? {
15131528
var backgroundTask: UIBackgroundTaskIdentifier?
1514-
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Remote Data Upload") {
1529+
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: name) {
15151530
guard let backgroundTask = backgroundTask else {return}
15161531
UIApplication.shared.endBackgroundTask(backgroundTask)
1517-
self.log.error("Remote Data Upload background task expired")
1532+
self.log.error("Background Task Expired: %{public}@", name)
15181533
}
15191534

1520-
self.remoteDataServicesManager.triggerUpload(for: triggeringType) {
1521-
if let backgroundTask = backgroundTask {
1522-
UIApplication.shared.endBackgroundTask(backgroundTask)
1535+
return backgroundTask
1536+
}
1537+
1538+
func beginBackgroundTask(name: String) async -> UIBackgroundTaskIdentifier? {
1539+
var backgroundTask: UIBackgroundTaskIdentifier?
1540+
backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: name) {
1541+
guard let backgroundTask = backgroundTask else {return}
1542+
Task {
1543+
await UIApplication.shared.endBackgroundTask(backgroundTask)
15231544
}
1545+
1546+
self.log.error("Background Task Expired: %{public}@", name)
15241547
}
1548+
1549+
return backgroundTask
1550+
}
1551+
1552+
func endBackgroundTask(_ backgroundTask: UIBackgroundTaskIdentifier?) async {
1553+
guard let backgroundTask else {return}
1554+
await UIApplication.shared.endBackgroundTask(backgroundTask)
1555+
}
1556+
1557+
func endBackgroundTask(_ backgroundTask: UIBackgroundTaskIdentifier?) {
1558+
guard let backgroundTask else {return}
1559+
UIApplication.shared.endBackgroundTask(backgroundTask)
15251560
}
15261561
}
15271562

Loop/Managers/NotificationManager.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ extension NotificationManager {
7878

7979
// MARK: - Notifications
8080

81+
@MainActor
8182
static func sendRemoteCommandExpiredNotification(timeExpired: TimeInterval) {
8283
let notification = UNMutableNotificationContent()
8384

@@ -134,6 +135,7 @@ extension NotificationManager {
134135
UNUserNotificationCenter.current().add(request)
135136
}
136137

138+
@MainActor
137139
static func sendRemoteBolusNotification(amount: Double) {
138140
let notification = UNMutableNotificationContent()
139141
let quantityFormatter = QuantityFormatter()
@@ -157,6 +159,7 @@ extension NotificationManager {
157159
UNUserNotificationCenter.current().add(request)
158160
}
159161

162+
@MainActor
160163
static func sendRemoteBolusFailureNotification(for error: Error, amount: Double) {
161164
let notification = UNMutableNotificationContent()
162165
let quantityFormatter = QuantityFormatter()
@@ -178,6 +181,7 @@ extension NotificationManager {
178181
UNUserNotificationCenter.current().add(request)
179182
}
180183

184+
@MainActor
181185
static func sendRemoteCarbEntryNotification(amountInGrams: Double) {
182186
let notification = UNMutableNotificationContent()
183187

@@ -198,6 +202,7 @@ extension NotificationManager {
198202
UNUserNotificationCenter.current().add(request)
199203
}
200204

205+
@MainActor
201206
static func sendRemoteCarbEntryFailureNotification(for error: Error, amountInGrams: Double) {
202207
let notification = UNMutableNotificationContent()
203208

0 commit comments

Comments
 (0)