Skip to content

Commit 48e1d2d

Browse files
mpangburnps2
authored andcommitted
Implement glucose schedule override (pre-meal/workout) in Watch App (#658)
* Implement glucose schedule override (pre-meal/workout) in Watch App * Watch button page transition code cleanup * Revise Watch app override buttons * Update watch UI only upon error after sending override context * Handle Watch override context message send errors received in non-serial order
1 parent 96eb8b2 commit 48e1d2d

File tree

25 files changed

+516
-50
lines changed

25 files changed

+516
-50
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// GlucoseRangeScheduleOverrideUserInfo.swift
3+
// Loop
4+
//
5+
// Created by Michael Pangburn on 12/30/17.
6+
// Copyright © 2017 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
12+
struct GlucoseRangeScheduleOverrideUserInfo {
13+
enum Context: Int {
14+
case workout
15+
case preMeal
16+
17+
static var allContexts: [Context] {
18+
return [.workout, .preMeal]
19+
}
20+
}
21+
22+
let context: Context
23+
let startDate: Date
24+
let endDate: Date?
25+
26+
var effectiveEndDate: Date {
27+
return endDate ?? .distantFuture
28+
}
29+
}
30+
31+
extension GlucoseRangeScheduleOverrideUserInfo: RawRepresentable {
32+
typealias RawValue = [String: Any]
33+
34+
static let version = 1
35+
static let name = "GlucoseRangeScheduleOverrideUserInfo"
36+
37+
init?(rawValue: RawValue) {
38+
guard rawValue["v"] as? Int == type(of: self).version && rawValue["name"] as? String == GlucoseRangeScheduleOverrideUserInfo.name,
39+
let contextRawValue = rawValue["context"] as? Int,
40+
let context = Context(rawValue: contextRawValue),
41+
let startDate = rawValue["startDate"] as? Date else
42+
{
43+
return nil
44+
}
45+
46+
self.context = context
47+
self.startDate = startDate
48+
self.endDate = rawValue["endDate"] as? Date
49+
}
50+
51+
var rawValue: RawValue {
52+
var raw: RawValue = [
53+
"v": type(of: self).version,
54+
"name": type(of: self).name,
55+
"context": context.rawValue,
56+
"startDate": startDate
57+
]
58+
59+
if let endDate = endDate {
60+
raw["endDate"] = endDate
61+
}
62+
63+
return raw
64+
}
65+
66+
/// The "raw value" of an override message intended to clear any active override
67+
static let clearOverride: RawValue = [
68+
"v": version,
69+
"name": name
70+
]
71+
}

Common/Models/WatchContext.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ final class WatchContext: NSObject, RawRepresentable {
2222
var eventualGlucose: HKQuantity?
2323
var glucoseDate: Date?
2424

25+
var glucoseRangeScheduleOverride: GlucoseRangeScheduleOverrideUserInfo?
26+
var configuredOverrideContexts: [GlucoseRangeScheduleOverrideUserInfo.Context] = []
27+
2528
var loopLastRunDate: Date?
2629
var lastNetTempBasalDose: Double?
2730
var lastNetTempBasalDate: Date?
@@ -66,6 +69,16 @@ final class WatchContext: NSObject, RawRepresentable {
6669
glucoseTrendRawValue = rawValue["gt"] as? Int
6770
glucoseDate = rawValue["gd"] as? Date
6871

72+
if let overrideUserInfoRawValue = rawValue["grsoc"] as? GlucoseRangeScheduleOverrideUserInfo.RawValue,
73+
let overrideUserInfo = GlucoseRangeScheduleOverrideUserInfo(rawValue: overrideUserInfoRawValue)
74+
{
75+
glucoseRangeScheduleOverride = overrideUserInfo
76+
}
77+
78+
if let configuredOverrideContextsRawValues = rawValue["coc"] as? [GlucoseRangeScheduleOverrideUserInfo.Context.RawValue] {
79+
configuredOverrideContexts = configuredOverrideContextsRawValues.flatMap(GlucoseRangeScheduleOverrideUserInfo.Context.init(rawValue:))
80+
}
81+
6982
IOB = rawValue["iob"] as? Double
7083
reservoir = rawValue["r"] as? Double
7184
reservoirPercentage = rawValue["rp"] as? Double
@@ -97,6 +110,8 @@ final class WatchContext: NSObject, RawRepresentable {
97110

98111
raw["gt"] = glucoseTrendRawValue
99112
raw["gd"] = glucoseDate
113+
raw["grsoc"] = glucoseRangeScheduleOverride?.rawValue
114+
raw["coc"] = configuredOverrideContexts.map { $0.rawValue }
100115
raw["iob"] = IOB
101116
raw["ld"] = loopLastRunDate
102117
raw["mb"] = maxBolus

Loop.xcodeproj/project.pbxproj

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@
9595
43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A567681C94880B00334FAC /* LoopDataManager.swift */; };
9696
43A5676B1C96155700334FAC /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */; };
9797
43A943761B926B7B0051FA24 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43A943741B926B7B0051FA24 /* Interface.storyboard */; };
98-
43A943781B926B7B0051FA24 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43A943771B926B7B0051FA24 /* Assets.xcassets */; };
9998
43A9437F1B926B7B0051FA24 /* WatchApp Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 43A9437E1B926B7B0051FA24 /* WatchApp Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
10099
43A943881B926B7B0051FA24 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A943871B926B7B0051FA24 /* ExtensionDelegate.swift */; };
101100
43A9438A1B926B7B0051FA24 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A943891B926B7B0051FA24 /* NotificationController.swift */; };
@@ -247,6 +246,9 @@
247246
7D70765E1FE06EE3004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076601FE06EE3004AC8EA /* Localizable.strings */; };
248247
7D7076631FE06EE4004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076651FE06EE4004AC8EA /* Localizable.strings */; };
249248
7D7076681FE0702F004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70766A1FE0702F004AC8EA /* InfoPlist.strings */; };
249+
894B91CD1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */; };
250+
894B91CE1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */; };
251+
894F71E21FFEC4D8007D365C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 894F71E11FFEC4D8007D365C /* Assets.xcassets */; };
250252
C10428971D17BAD400DD539A /* NightscoutUploadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */; };
251253
C10B28461EA9BA5E006EA1FC /* far_future_high_bg_forecast.json in Resources */ = {isa = PBXBuildFile; fileRef = C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */; };
252254
C11C87DD1E21E53500BB71D3 /* GlucoseThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */; };
@@ -482,7 +484,6 @@
482484
43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = "<group>"; };
483485
43A943721B926B7B0051FA24 /* WatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
484486
43A943751B926B7B0051FA24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
485-
43A943771B926B7B0051FA24 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
486487
43A9437E1B926B7B0051FA24 /* WatchApp Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "WatchApp Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
487488
43A943841B926B7B0051FA24 /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = "<group>"; };
488489
43A943871B926B7B0051FA24 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = "<group>"; };
@@ -625,6 +626,8 @@
625626
7DD382771F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Main.strings; sourceTree = "<group>"; };
626627
7DD382781F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainInterface.strings; sourceTree = "<group>"; };
627628
7DD382791F8DBFC60071272B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Interface.strings; sourceTree = "<group>"; };
629+
894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseRangeScheduleOverrideUserInfo.swift; sourceTree = "<group>"; };
630+
894F71E11FFEC4D8007D365C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
628631
C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NightscoutUploadKit.framework; path = Carthage/Build/iOS/NightscoutUploadKit.framework; sourceTree = "<group>"; };
629632
C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = far_future_high_bg_forecast.json; sourceTree = "<group>"; };
630633
C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_very_low_end_in_range.json; sourceTree = "<group>"; };
@@ -856,8 +859,8 @@
856859
43A943731B926B7B0051FA24 /* WatchApp */ = {
857860
isa = PBXGroup;
858861
children = (
862+
894F71E11FFEC4D8007D365C /* Assets.xcassets */,
859863
C1C73F0F1DE3D0270022FC89 /* InfoPlist.strings */,
860-
43A943771B926B7B0051FA24 /* Assets.xcassets */,
861864
43F5C2D61B92A4DC003EB13D /* Info.plist */,
862865
43A943741B926B7B0051FA24 /* Interface.storyboard */,
863866
);
@@ -1132,6 +1135,7 @@
11321135
children = (
11331136
435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */,
11341137
43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */,
1138+
894B91CC1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift */,
11351139
43EA285E1D50ED3D001BC233 /* GlucoseTrend.swift */,
11361140
435400331C9F878D00D5819C /* SetBolusUserInfo.swift */,
11371141
4F70C2111DE900EA006380B7 /* StatusExtensionContext.swift */,
@@ -1450,8 +1454,8 @@
14501454
isa = PBXResourcesBuildPhase;
14511455
buildActionMask = 2147483647;
14521456
files = (
1453-
43A943781B926B7B0051FA24 /* Assets.xcassets in Resources */,
14541457
C1C73F0D1DE3D0270022FC89 /* InfoPlist.strings in Resources */,
1458+
894F71E21FFEC4D8007D365C /* Assets.xcassets in Resources */,
14551459
43A943761B926B7B0051FA24 /* Interface.storyboard in Resources */,
14561460
);
14571461
runOnlyForDeploymentPostprocessing = 0;
@@ -1650,6 +1654,7 @@
16501654
4F08DE8F1E7BB871006741EA /* CollectionType+Loop.swift in Sources */,
16511655
435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */,
16521656
437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */,
1657+
894B91CD1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */,
16531658
43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */,
16541659
433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */,
16551660
43D2E8231F00425400AE5CBF /* BolusViewController+LoopDataManager.swift in Sources */,
@@ -1704,6 +1709,7 @@
17041709
4328E02A1CFBE2C500E199AA /* UIColor.swift in Sources */,
17051710
4328E01B1CFBE1DA00E199AA /* BolusInterfaceController.swift in Sources */,
17061711
4328E02B1CFBE2C500E199AA /* WKAlertAction.swift in Sources */,
1712+
894B91CE1FF9F45900DA65F5 /* GlucoseRangeScheduleOverrideUserInfo.swift in Sources */,
17071713
4328E0281CFBE2C500E199AA /* CLKComplicationTemplate.swift in Sources */,
17081714
4328E01E1CFBE25F00E199AA /* AddCarbsInterfaceController.swift in Sources */,
17091715
43846ADB1D91057000799272 /* ContextUpdatable.swift in Sources */,

Loop/Extensions/GlucoseRangeSchedule.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ extension GlucoseRangeSchedule {
2323
return override.isActive()
2424
}
2525

26+
var activeOverrideContext: GlucoseRangeSchedule.Override.Context? {
27+
guard let override = override, override.isActive() else {
28+
return nil
29+
}
30+
31+
return override.context
32+
}
33+
34+
var configuredOverrideContexts: [GlucoseRangeSchedule.Override.Context] {
35+
var contexts: [GlucoseRangeSchedule.Override.Context] = []
36+
for (context, range) in overrideRanges where !range.isZero {
37+
contexts.append(context)
38+
}
39+
40+
return contexts
41+
}
42+
2643
func minQuantity(at date: Date) -> HKQuantity {
2744
return HKQuantity(unit: unit, doubleValue: value(at: date).minValue)
2845
}

Loop/Managers/WatchDataManager.swift

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,36 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
3737
}
3838
}()
3939

40+
private var lastActiveOverrideContext: GlucoseRangeSchedule.Override.Context?
41+
private var lastConfiguredOverrideContexts: [GlucoseRangeSchedule.Override.Context] = []
42+
4043
@objc private func updateWatch(_ notification: Notification) {
4144
guard
42-
let rawContext = notification.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue,
43-
let context = LoopDataManager.LoopUpdateContext(rawValue: rawContext),
44-
case .tempBasal = context,
45+
let rawUpdateContext = notification.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue,
46+
let updateContext = LoopDataManager.LoopUpdateContext(rawValue: rawUpdateContext),
4547
let session = watchSession
4648
else {
4749
return
4850
}
4951

52+
switch updateContext {
53+
case .tempBasal:
54+
break
55+
case .preferences:
56+
let activeOverrideContext = deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.activeOverrideContext
57+
let configuredOverrideContexts = deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.configuredOverrideContexts ?? []
58+
defer {
59+
lastActiveOverrideContext = activeOverrideContext
60+
lastConfiguredOverrideContexts = configuredOverrideContexts
61+
}
62+
63+
guard activeOverrideContext != lastActiveOverrideContext || configuredOverrideContexts != lastConfiguredOverrideContexts else {
64+
return
65+
}
66+
default:
67+
return
68+
}
69+
5070
switch session.activationState {
5171
case .notActivated, .inactive:
5272
session.activate()
@@ -109,6 +129,20 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
109129
context.recommendedBolusDose = try? state.recommendBolus().amount
110130
context.maxBolus = manager.settings.maximumBolus
111131

132+
if let glucoseTargetRangeSchedule = manager.settings.glucoseTargetRangeSchedule {
133+
if let override = glucoseTargetRangeSchedule.override {
134+
context.glucoseRangeScheduleOverride = GlucoseRangeScheduleOverrideUserInfo(
135+
context: override.context.correspondingUserInfoContext,
136+
startDate: override.start,
137+
endDate: override.end
138+
)
139+
}
140+
141+
let configuredOverrideContexts = self.deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.configuredOverrideContexts ?? []
142+
let configuredUserInfoOverrideContexts = configuredOverrideContexts.map { $0.correspondingUserInfoContext }
143+
context.configuredOverrideContexts = configuredUserInfoOverrideContexts
144+
}
145+
112146
if let trend = self.deviceDataManager.sensorInfo?.trendType {
113147
context.glucoseTrendRawValue = trend.rawValue
114148
}
@@ -144,7 +178,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
144178

145179
// MARK: WCSessionDelegate
146180

147-
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String: Any]) -> Void) {
181+
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
148182
switch message["name"] as? String {
149183
case CarbEntryUserInfo.name?:
150184
addCarbEntryFromWatchMessage(message) { (units) in
@@ -160,12 +194,32 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
160194
}
161195

162196
replyHandler([:])
197+
case GlucoseRangeScheduleOverrideUserInfo.name?:
198+
if let overrideUserInfo = GlucoseRangeScheduleOverrideUserInfo(rawValue: message) {
199+
let overrideContext = overrideUserInfo.context.correspondingOverrideContext
200+
201+
// update the recorded last active override context prior to enabling the actual override
202+
// to prevent the Watch context being unnecessarily sent in response to the override being enabled
203+
let previousActiveOverrideContext = lastActiveOverrideContext
204+
lastActiveOverrideContext = overrideContext
205+
let overrideSuccess = deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(overrideContext, from: overrideUserInfo.startDate, until: overrideUserInfo.effectiveEndDate)
206+
207+
if overrideSuccess == false {
208+
lastActiveOverrideContext = previousActiveOverrideContext
209+
}
210+
211+
replyHandler([:])
212+
} else {
213+
lastActiveOverrideContext = nil
214+
deviceDataManager.loopManager.settings.glucoseTargetRangeSchedule?.clearOverride()
215+
replyHandler([:])
216+
}
163217
default:
164218
replyHandler([:])
165219
}
166220
}
167221

168-
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) {
222+
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
169223
addCarbEntryFromWatchMessage(userInfo)
170224
}
171225

@@ -195,5 +249,26 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
195249
watchSession?.delegate = self
196250
watchSession?.activate()
197251
}
252+
}
253+
254+
fileprivate extension GlucoseRangeSchedule.Override.Context {
255+
var correspondingUserInfoContext: GlucoseRangeScheduleOverrideUserInfo.Context {
256+
switch self {
257+
case .preMeal:
258+
return .preMeal
259+
case .workout:
260+
return .workout
261+
}
262+
}
263+
}
198264

265+
fileprivate extension GlucoseRangeScheduleOverrideUserInfo.Context {
266+
var correspondingOverrideContext: GlucoseRangeSchedule.Override.Context {
267+
switch self {
268+
case .preMeal:
269+
return .preMeal
270+
case .workout:
271+
return .workout
272+
}
273+
}
199274
}

0 commit comments

Comments
 (0)