Skip to content

Commit ba6c7f3

Browse files
committed
Remote Action Models
1 parent f6efd72 commit ba6c7f3

8 files changed

+275
-417
lines changed

Loop.xcodeproj/project.pbxproj

+4-4
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@
412412
A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */; };
413413
A9DFAFB324F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */; };
414414
A9DFAFB524F048A000950D1E /* WatchHistoricalCarbsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */; };
415-
A9E8A80528A7CAC000C0A8A4 /* RemoteCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */; };
415+
A9E8A80528A7CAC000C0A8A4 /* RemoteActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */; };
416416
A9F5F1F5251050EC00E7C8A4 /* ZipArchiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */; };
417417
A9F66FC3247F451500096EA7 /* UIDevice+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F66FC2247F451500096EA7 /* UIDevice+Loop.swift */; };
418418
A9F703732489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */; };
@@ -1407,7 +1407,7 @@
14071407
A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogTests.swift; sourceTree = "<group>"; };
14081408
A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbBackfillRequestUserInfoTests.swift; sourceTree = "<group>"; };
14091409
A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHistoricalCarbsTests.swift; sourceTree = "<group>"; };
1410-
A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandTests.swift; sourceTree = "<group>"; };
1410+
A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteActionTests.swift; sourceTree = "<group>"; };
14111411
A9F5F1F4251050EC00E7C8A4 /* ZipArchiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchiveTests.swift; sourceTree = "<group>"; };
14121412
A9F66FC2247F451500096EA7 /* UIDevice+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Loop.swift"; sourceTree = "<group>"; };
14131413
A9F703722489BC8500C98AD8 /* CarbStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbStore+SimulatedCoreData.swift"; sourceTree = "<group>"; };
@@ -2803,7 +2803,7 @@
28032803
C19008FF252271BB00721625 /* SimpleBolusCalculatorTests.swift */,
28042804
A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */,
28052805
A9BD28E6272226B40071DF15 /* TestLocalizedError.swift */,
2806-
A9E8A80428A7CAC000C0A8A4 /* RemoteCommandTests.swift */,
2806+
A9E8A80428A7CAC000C0A8A4 /* RemoteActionTests.swift */,
28072807
);
28082808
path = Models;
28092809
sourceTree = "<group>";
@@ -4121,7 +4121,7 @@
41214121
A96DAC2A2838EF8A00D94E38 /* DiagnosticLogTests.swift in Sources */,
41224122
A9DAE7D02332D77F006AE942 /* LoopTests.swift in Sources */,
41234123
E93E86B024DDE1BD00FF40C8 /* MockGlucoseStore.swift in Sources */,
4124-
A9E8A80528A7CAC000C0A8A4 /* RemoteCommandTests.swift in Sources */,
4124+
A9E8A80528A7CAC000C0A8A4 /* RemoteActionTests.swift in Sources */,
41254125
1DFE9E172447B6270082C280 /* UserNotificationAlertSchedulerTests.swift in Sources */,
41264126
B4BC56382518DEA900373647 /* CGMStatusHUDViewModelTests.swift in Sources */,
41274127
C1900900252271BB00721625 /* SimpleBolusCalculatorTests.swift in Sources */,

Loop/Managers/DeviceDataManager.swift

+132-128
Large diffs are not rendered by default.

Loop/Managers/NotificationManager.swift

+5
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

Loop/Managers/RemoteDataServicesManager.swift

+8
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,14 @@ final class RemoteDataServicesManager {
209209
completion()
210210
}
211211
}
212+
213+
func triggerUpload(for triggeringType: RemoteDataType) async {
214+
return await withCheckedContinuation { continuation in
215+
triggerUpload(for: triggeringType) {
216+
continuation.resume(returning: ())
217+
}
218+
}
219+
}
212220
}
213221

214222
extension RemoteDataServicesManager {

Loop/Models/LoopConstants.swift

+4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ enum LoopConstants {
2121

2222
static let validManualGlucoseEntryRange = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 10)...HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 600)
2323

24+
static let minCarbAbsorptionTime = TimeInterval(minutes: 30)
2425
static let maxCarbAbsorptionTime = TimeInterval(hours: 8)
26+
27+
static let maxCarbEntryPastTime = TimeInterval(hours: (-12))
28+
static let maxCarbEntryFutureTime = TimeInterval(hours: 1)
2529

2630

2731
// MARK - Display settings

Loop/Models/RemoteCommand.swift

+14-60
Original file line numberDiff line numberDiff line change
@@ -8,78 +8,34 @@
88

99
import Foundation
1010
import LoopKit
11-
import HealthKit
12-
13-
public enum RemoteCommandError: LocalizedError {
14-
case expired
15-
case invalidOTP
16-
case missingMaxBolus
17-
case exceedsMaxBolus
18-
case exceedsMaxCarbs
19-
case invalidCarbs
20-
21-
public var errorDescription: String? {
22-
get {
23-
switch self {
24-
case .expired:
25-
return NSLocalizedString("Expired", comment: "Remote command error description: expired.")
26-
case .invalidOTP:
27-
return NSLocalizedString("Invalid OTP", comment: "Remote command error description: invalid OTP.")
28-
case .missingMaxBolus:
29-
return NSLocalizedString("Missing maximum allowed bolus in settings", comment: "Remote command error description: missing maximum bolus in settings.")
30-
case .exceedsMaxBolus:
31-
return NSLocalizedString("Exceeds maximum allowed bolus in settings", comment: "Remote command error description: bolus exceeds maximum bolus in settings.")
32-
case .exceedsMaxCarbs:
33-
return NSLocalizedString("Exceeds maximum allowed carbs", comment: "Remote command error description: carbs exceed maximum amount.")
34-
case .invalidCarbs:
35-
return NSLocalizedString("Invalid carb amount", comment: "Remote command error description: invalid carb amount.")
36-
}
37-
}
38-
}
39-
}
40-
41-
42-
enum RemoteCommand {
43-
case temporaryScheduleOverride(TemporaryScheduleOverride)
44-
case cancelTemporaryOverride
45-
case bolusEntry(Double)
46-
case carbsEntry(NewCarbEntry)
47-
}
48-
4911

5012
// Push Notifications
51-
extension RemoteCommand {
52-
static func createRemoteCommand(notification: [String: Any], allowedPresets: [TemporaryScheduleOverridePreset], defaultAbsorptionTime: TimeInterval, nowDate: Date = Date()) -> Result<RemoteCommand, RemoteCommandParseError> {
13+
extension RemoteAction {
14+
static func createRemoteAction(notification: [String: Any]) -> Result<RemoteAction, RemoteCommandParseError> {
5315
if let overrideName = notification["override-name"] as? String,
54-
let preset = allowedPresets.first(where: { $0.name == overrideName }),
5516
let remoteAddress = notification["remote-address"] as? String
5617
{
57-
var override = preset.createOverride(enactTrigger: .remote(remoteAddress))
18+
var overrideTime: TimeInterval? = nil
5819
if let overrideDurationMinutes = notification["override-duration-minutes"] as? Double {
59-
override.duration = .finite(TimeInterval(minutes: overrideDurationMinutes))
20+
overrideTime = TimeInterval(minutes: overrideDurationMinutes)
6021
}
61-
return .success(.temporaryScheduleOverride(override))
62-
} else if let _ = notification["cancel-temporary-override"] as? String {
63-
return .success(.cancelTemporaryOverride)
22+
return .success(.temporaryScheduleOverride(RemoteOverrideAction(name: overrideName, durationTime: overrideTime, remoteAddress: remoteAddress)))
23+
} else if let _ = notification["cancel-temporary-override"] as? String,
24+
let remoteAddress = notification["remote-address"] as? String
25+
{
26+
return .success(.cancelTemporaryOverride(RemoteOverrideCancelAction(remoteAddress: remoteAddress)))
6427
} else if let bolusValue = notification["bolus-entry"] as? Double {
65-
return .success(.bolusEntry(bolusValue))
28+
return .success(.bolusEntry(RemoteBolusAction(amountInUnits: bolusValue)))
6629
} else if let carbsValue = notification["carbs-entry"] as? Double {
67-
68-
let minAbsorptionTime = TimeInterval(hours: 0.5)
69-
let maxAbsorptionTime = LoopConstants.maxCarbAbsorptionTime
7030

71-
var absorptionTime = defaultAbsorptionTime
31+
var absorptionTime: TimeInterval? = nil
7232
if let absorptionOverrideInHours = notification["absorption-time"] as? Double {
7333
absorptionTime = TimeInterval(hours: absorptionOverrideInHours)
7434
}
7535

76-
if absorptionTime < minAbsorptionTime || absorptionTime > maxAbsorptionTime {
77-
return .failure(RemoteCommandParseError.invalidAbsorptionSeconds(absorptionTime))
78-
}
79-
80-
let quantity = HKQuantity(unit: .gram(), doubleValue: carbsValue)
36+
var foodType = notification["food-type"] as? String ?? nil
8137

82-
var startDate = nowDate
38+
var startDate: Date? = nil
8339
if let notificationStartTimeString = notification["start-time"] as? String {
8440
let formatter = ISO8601DateFormatter()
8541
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -90,16 +46,14 @@ extension RemoteCommand {
9046
}
9147
}
9248

93-
let newEntry = NewCarbEntry(quantity: quantity, startDate: startDate, foodType: "", absorptionTime: absorptionTime)
94-
return .success(.carbsEntry(newEntry))
49+
return .success(.carbsEntry(RemoteCarbAction(amountInGrams: carbsValue, absorptionTime: absorptionTime, foodType: foodType, startDate: startDate)))
9550
} else {
9651
return .failure(RemoteCommandParseError.unhandledNotication("\(notification)"))
9752
}
9853
}
9954

10055
enum RemoteCommandParseError: LocalizedError {
10156
case invalidStartTime(String)
102-
case invalidAbsorptionSeconds(Double)
10357
case unhandledNotication(String)
10458
}
10559
}
+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// RemoteActionTests.swift
3+
// LoopTests
4+
//
5+
// Created by Bill Gestrich on 8/13/22.
6+
// Copyright © 2022 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import XCTest
10+
import HealthKit
11+
@testable import Loop
12+
import LoopKit
13+
14+
class RemoteActionTests: XCTestCase {
15+
16+
override func setUpWithError() throws {
17+
}
18+
19+
override func tearDownWithError() throws {
20+
}
21+
22+
23+
//MARK: Carb Entry Command
24+
25+
func testParseCarbEntryNotification_ValidPayload_Succeeds() throws {
26+
27+
//Arrange
28+
let expectedStartDateString = "2022-08-14T03:08:00.000Z"
29+
let expectedCarbsInGrams = 15.0
30+
let expectedDate = dateFormatter().date(from: expectedStartDateString)!
31+
let expectedAbsorptionTimeInHours = 3.0
32+
let expectedFoodType = "🍕"
33+
let otp = 12345
34+
let notification: [String: Any] = [
35+
"carbs-entry":expectedCarbsInGrams,
36+
"absorption-time": expectedAbsorptionTimeInHours,
37+
"food-type": expectedFoodType,
38+
"otp": otp,
39+
"start-time": expectedStartDateString
40+
]
41+
42+
//Act
43+
let action = try RemoteAction.createRemoteAction(notification: notification).get()
44+
45+
//Assert
46+
guard case .carbsEntry(let carbEntry) = action else {
47+
XCTFail("Incorrect case")
48+
return
49+
}
50+
XCTAssertEqual(carbEntry.startDate, expectedDate)
51+
XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours))
52+
XCTAssertEqual(carbEntry.amountInGrams, expectedCarbsInGrams)
53+
XCTAssertEqual(expectedFoodType, carbEntry.foodType)
54+
}
55+
56+
func testParseCarbEntryNotification_MissingCreatedDate_Succeeds() throws {
57+
58+
//Arrange
59+
let expectedCarbsInGrams = 15.0
60+
let expectedAbsorptionTimeInHours = 3.0
61+
let otp = 12345
62+
let notification: [String: Any] = [
63+
"carbs-entry":expectedCarbsInGrams,
64+
"absorption-time": expectedAbsorptionTimeInHours,
65+
"otp": otp
66+
]
67+
68+
//Act
69+
let action = try RemoteAction.createRemoteAction(notification: notification).get()
70+
71+
//Assert
72+
guard case .carbsEntry(let carbEntry) = action else {
73+
XCTFail("Incorrect case")
74+
return
75+
}
76+
77+
XCTAssertEqual(carbEntry.startDate, nil)
78+
XCTAssertEqual(carbEntry.absorptionTime, TimeInterval(hours: expectedAbsorptionTimeInHours))
79+
XCTAssertEqual(carbEntry.amountInGrams, expectedCarbsInGrams)
80+
}
81+
82+
func testParseCarbEntryNotification_InvalidCreatedDate_Fails() throws {
83+
84+
//Arrange
85+
let expectedCarbsInGrams = 15.0
86+
let expectedAbsorptionTimeInHours = 3.0
87+
let otp = 12345
88+
let notification: [String: Any] = [
89+
"carbs-entry": expectedCarbsInGrams,
90+
"absorption-time":expectedAbsorptionTimeInHours,
91+
"otp": otp,
92+
"start-time": "invalid-date-string"
93+
]
94+
95+
//Act + Assert
96+
XCTAssertThrowsError(try RemoteAction.createRemoteAction(notification: notification).get())
97+
}
98+
99+
100+
//MARK: Utils
101+
102+
func dateFormatter() -> ISO8601DateFormatter {
103+
let formatter = ISO8601DateFormatter()
104+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
105+
return formatter
106+
}
107+
108+
}

0 commit comments

Comments
 (0)