Skip to content

Commit 31f8ac6

Browse files
committed
remote temp targets part 1
1 parent 9cb805d commit 31f8ac6

File tree

6 files changed

+190
-2
lines changed

6 files changed

+190
-2
lines changed

Common/Models/GlucoseRangeScheduleOverrideUserInfo.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ struct GlucoseRangeScheduleOverrideUserInfo {
1313
enum Context: Int {
1414
case workout
1515
case preMeal
16+
case remoteTempTarget
1617

1718
static var allContexts: [Context] {
18-
return [.workout, .preMeal]
19+
return [.workout, .preMeal, .remoteTempTarget]
1920
}
2021
}
2122

Loop/Managers/DeviceDataManager.swift

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,12 @@ final class DeviceDataManager {
352352
private func updateReservoirVolume(_ units: Double, at date: Date, withTimeLeft timeLeft: TimeInterval?) {
353353
loopManager.addReservoirValue(units, at: date) { (result) in
354354
/// TODO: Isolate to queue
355+
//////
356+
// update BG correction range overrides via NS
357+
// this call may be more appropriate somewhere
358+
let allowremoteTempTargets : Bool = true
359+
if allowremoteTempTargets == true {self.setNStemp()}
360+
/////
355361

356362
switch result {
357363
case .failure(let error):
@@ -737,6 +743,135 @@ final class DeviceDataManager {
737743
// MARK: - Status Extension
738744

739745
fileprivate var statusExtensionManager: StatusExtensionDataManager!
746+
747+
748+
//////////////////////////////////////////
749+
// MARK: - Set Temp Targets From NS
750+
// by LoopKit Authors Ken Stack, Katie DiSimone
751+
752+
struct NStempTarget : Codable {
753+
let created_at : String
754+
let duration : Int
755+
let targetBottom : Double?
756+
let targetTop : Double?
757+
}
758+
759+
func doubleIsEqual(_ a: Double, _ b: Double, _ precision: Double) -> Bool {
760+
return fabs(a - b) < precision
761+
}
762+
763+
func setNStemp () {
764+
// data from URL logic from modified http://mrgott.com/swift-programing/33-rest-api-in-swift-4-using-urlsession-and-jsondecode
765+
//look at users nightscout treatments collection and implement temporary BG targets using an override called remoteTempTarget that was added to Loopkit
766+
//user set overrides always have precedence
767+
if self.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.workout) == true || self.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.preMeal) == true {return}
768+
let nightscoutService = remoteDataManager.nightscoutService
769+
guard let nssite = nightscoutService.siteURL?.absoluteString else {return}
770+
let formatter = ISO8601DateFormatter()
771+
formatter.formatOptions = [.withFullDate,
772+
.withTime,
773+
.withDashSeparatorInDate,
774+
.withColonSeparatorInTime]
775+
//how far back to look for valid treatments in hours
776+
let treatmentWindow : TimeInterval = TimeInterval(.hours(24))
777+
let now : Date = Date()
778+
let lasteventDate : Date = now - treatmentWindow
779+
//only consider treatments from now back to treatmentWindow
780+
let urlString = nssite + "/api/v1/treatments.json?find[eventType]=Temporary%20Target&find[created_at][$gte]="+formatter.string(from: lasteventDate)+"&find[created_at][$lte]=" + formatter.string(from: now)
781+
guard let url = URL(string: urlString) else {
782+
return
783+
}
784+
let session = URLSession.shared
785+
var request = URLRequest(url: url)
786+
request.httpMethod = "GET"
787+
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
788+
session.dataTask(with: request as URLRequest) { (data, response, error) in
789+
if error != nil {
790+
return
791+
}
792+
guard let data = data else { return }
793+
794+
do {
795+
let temptargets = try JSONDecoder().decode([NStempTarget].self, from: data)
796+
//check to see if we found some recent temp targets
797+
if temptargets.count == 0 {return}
798+
//find the index of the most recent temptargets - sort by date
799+
var cdates = [Date]()
800+
for item in temptargets {
801+
cdates.append(formatter.date(from: (item.created_at as String))!)
802+
}
803+
let last = temptargets[cdates.index(of:cdates.max()!) as! Int]
804+
//if duration is 0 we dont care about minmax levels, if not we need them to exist as Double
805+
//NS doesnt check to see if a duration is created but no targets exist - so we have too
806+
if last.duration != 0 {
807+
guard last.targetBottom != nil else {return}
808+
guard last.targetTop != nil else {return}
809+
}
810+
811+
//cancel any prior remoteTemp if last duration = 0 and remote temp is active else return anyway
812+
if last.duration < 1 {
813+
if self.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.remoteTempTarget) == true {
814+
self.loopManager.settings.glucoseTargetRangeSchedule?.clearOverride(matching: .remoteTempTarget)
815+
NotificationManager.remoteTempSetNotification(duration:last.duration, lowTarget: 0.0, highTarget: 0.0)
816+
}
817+
return
818+
}
819+
// set the remote temp if it's valid and not already set. Handle the nil issue as well
820+
let endlastTemp = cdates.max()! + TimeInterval(.minutes(Double(last.duration)))
821+
if Date() < endlastTemp {
822+
let NStargetUnit = HKUnit.milligramsPerDeciliter()
823+
let userUnit = self.loopManager.settings.glucoseTargetRangeSchedule?.unit
824+
//convert NS temp targets to an HKQuanity with units and set limits (low of 70 mg/dL, high of 300 mg/dL)
825+
//ns temps are always given in mg/dL
826+
let lowerTarget : HKQuantity = HKQuantity(unit : NStargetUnit, doubleValue:max(70.0,last.targetBottom as! Double))
827+
let upperTarget : HKQuantity = HKQuantity(unit : NStargetUnit, doubleValue:min(300.0,last.targetTop as! Double))
828+
//set the temp if override isn't enabled or is nil ie never enabled
829+
if self.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.remoteTempTarget) != true {
830+
self.setRemoteTemp(lowerTarget: lowerTarget, upperTarget: upperTarget, userUnit: userUnit!, endlastTemp: endlastTemp, duration: last.duration)
831+
return
832+
}
833+
let currentRange = self.loopManager.settings.glucoseTargetRangeSchedule?.overrideRanges[.remoteTempTarget]!
834+
let activeDates = self.loopManager.settings.glucoseTargetRangeSchedule!.override?.activeDates
835+
///this handles the case if a remote temp was canceled by a different temp, but the app restarts in between
836+
if currentRange == nil || activeDates == nil {
837+
self.setRemoteTemp(lowerTarget: lowerTarget, upperTarget: upperTarget, userUnit: userUnit!, endlastTemp: endlastTemp, duration: last.duration)
838+
return
839+
}
840+
//if anything has changed - ranges or duration, reset the temp
841+
if self.doubleIsEqual((currentRange?.minValue)!, lowerTarget.doubleValue(for: userUnit!), 0.01) == false || self.doubleIsEqual((currentRange?.maxValue)!, upperTarget.doubleValue(for: userUnit!), 0.01) == false || abs(activeDates!.end.timeIntervalSince(endlastTemp)) > TimeInterval(.minutes(1)) {
842+
843+
self.setRemoteTemp(lowerTarget: lowerTarget, upperTarget: upperTarget, userUnit: userUnit!, endlastTemp: endlastTemp, duration: last.duration)
844+
return
845+
846+
}
847+
else
848+
{
849+
//print(" temp already running no changes")
850+
}
851+
852+
853+
}
854+
else {
855+
//last temp has expired - do a hard cancel to fix the UI- it should cancel itself but it doesnt
856+
if self.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.remoteTempTarget) == true {
857+
self.loopManager.settings.glucoseTargetRangeSchedule?.clearOverride(matching: .remoteTempTarget)
858+
}
859+
}
860+
} catch let jsonError {
861+
return
862+
}
863+
}.resume()
864+
}
865+
866+
func setRemoteTemp (lowerTarget: HKQuantity, upperTarget: HKQuantity, userUnit:HKUnit, endlastTemp: Date, duration: Int) {
867+
self.loopManager.settings.glucoseTargetRangeSchedule?.overrideRanges[.remoteTempTarget] = DoubleRange(minValue: lowerTarget.doubleValue(for: userUnit), maxValue: upperTarget.doubleValue(for: userUnit))
868+
let remoteTempSet = self.loopManager.settings.glucoseTargetRangeSchedule?.setOverride(.remoteTempTarget, until:endlastTemp)
869+
if remoteTempSet! {
870+
NotificationManager.remoteTempSetNotification(duration:duration , lowTarget: lowerTarget.doubleValue(for: userUnit), highTarget:upperTarget.doubleValue(for: userUnit))
871+
}
872+
}
873+
874+
//////////////////////////
740875

741876
// MARK: - Initialization
742877

Loop/Managers/NotificationManager.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct NotificationManager {
1919
case pumpBatteryLow
2020
case pumpReservoirEmpty
2121
case pumpReservoirLow
22+
case remoteTemp
2223
}
2324

2425
enum Action: String {
@@ -169,6 +170,32 @@ struct NotificationManager {
169170

170171
UNUserNotificationCenter.current().add(request)
171172
}
173+
174+
static func remoteTempSetNotification(duration: Int, lowTarget: Double, highTarget:Double) {
175+
let notification = UNMutableNotificationContent()
176+
if duration < 1 as Int {
177+
notification.title = NSLocalizedString("Remote Temporary Target Canceled ", comment: "The notification title for a remote temp being canceled")
178+
}
179+
else
180+
{
181+
let lowTargetString = NumberFormatter.localizedString(from: NSNumber(value: lowTarget), number: .decimal)
182+
let highTargetString = NumberFormatter.localizedString(from: NSNumber(value: highTarget), number: .decimal)
183+
184+
notification.title = NSLocalizedString("Remote Temporary Target Set ", comment: "The notification title for Remote Target Being Set")
185+
186+
notification.body = String(format: NSLocalizedString(" LowTarget: %1$@ HighTarget: %2$@ Duration: %3$@", comment: "Low Target high Target"), lowTargetString, highTargetString, String(duration))
187+
}
188+
notification.sound = UNNotificationSound.default()
189+
notification.categoryIdentifier = Category.remoteTemp.rawValue
190+
let request = UNNotificationRequest(
191+
identifier: Category.remoteTemp.rawValue,
192+
content: notification,
193+
trigger: nil
194+
)
195+
196+
UNUserNotificationCenter.current().add(request)
197+
198+
}
172199

173200
static func sendPumpReservoirLowNotificationForAmount(_ units: Double, andTimeRemaining remaining: TimeInterval?) {
174201
let notification = UNMutableNotificationContent()

Loop/Managers/WatchDataManager.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ fileprivate extension GlucoseRangeSchedule.Override.Context {
311311
return .preMeal
312312
case .workout:
313313
return .workout
314+
case .remoteTempTarget:
315+
return .remoteTempTarget
314316
}
315317
}
316318
}
@@ -322,6 +324,8 @@ fileprivate extension GlucoseRangeScheduleOverrideUserInfo.Context {
322324
return .preMeal
323325
case .workout:
324326
return .workout
327+
case .remoteTempTarget:
328+
return .remoteTempTarget
325329
}
326330
}
327331
}

Loop/View Controllers/StatusTableViewController.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,8 @@ final class StatusTableViewController: ChartsTableViewController {
359359

360360
workoutMode = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.workout)
361361
preMealMode = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.preMeal)
362-
362+
remoteTempTargetMode = deviceManager.loopManager.settings.glucoseTargetRangeSchedule?.overrideEnabledForContext(.remoteTempTarget)
363+
363364
reloadGroup.notify(queue: .main) {
364365
/// Update the chart data
365366

@@ -632,6 +633,20 @@ final class StatusTableViewController: ChartsTableViewController {
632633
}
633634
}
634635
}
636+
637+
private var remoteTempTargetMode: Bool? = nil {
638+
didSet {
639+
guard oldValue != remoteTempTargetMode else {
640+
return
641+
}
642+
643+
// if let workoutMode = workoutMode {
644+
// toolbarItems![6] = createWorkoutButtonItem(selected: workoutMode)
645+
// } else {
646+
// toolbarItems![6].isEnabled = false
647+
// }
648+
}
649+
}
635650

636651
// MARK: - Table view data source
637652

WatchApp Extension/Controllers/StatusInterfaceController.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable {
3333
private lazy var preMealButtonGroup = ButtonGroup(button: preMealButton, image: preMealButtonImage, background: preMealButtonBackground, onBackgroundColor: .carbsColor, offBackgroundColor: .darkCarbsColor)
3434

3535
private lazy var workoutButtonGroup = ButtonGroup(button: workoutButton, image: workoutButtonImage, background: workoutButtonBackground, onBackgroundColor: .workoutColor, offBackgroundColor: .darkWorkoutColor)
36+
37+
private lazy var remoteTempTargetButtonGroup = ButtonGroup(button: workoutButton, image: workoutButtonImage, background: workoutButtonBackground, onBackgroundColor: .workoutColor, offBackgroundColor: .darkWorkoutColor)
3638

3739
private var lastOverrideContext: GlucoseRangeScheduleOverrideUserInfo.Context?
3840

@@ -231,6 +233,8 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable {
231233
case .workout?:
232234
preMealButtonGroup.turnOff()
233235
workoutButtonGroup.state = .on
236+
case .remoteTempTarget?:
237+
()
234238
case nil:
235239
preMealButtonGroup.turnOff()
236240
workoutButtonGroup.turnOff()
@@ -243,6 +247,8 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable {
243247
return preMealButtonGroup
244248
case .workout:
245249
return workoutButtonGroup
250+
case .remoteTempTarget:
251+
return remoteTempTargetButtonGroup
246252
}
247253
}
248254

0 commit comments

Comments
 (0)