Skip to content

Commit

Permalink
User configurable pod expiration warning
Browse files Browse the repository at this point in the history
  • Loading branch information
ps2 committed Apr 12, 2019
1 parent 9e10165 commit 97acf9d
Show file tree
Hide file tree
Showing 20 changed files with 468 additions and 816 deletions.
1 change: 1 addition & 0 deletions Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github "LoopKit/LoopKit" "dev"
github "maxkonovalov/MKRingProgressView" ~> 2.2
5 changes: 5 additions & 0 deletions OmniKit/Model/Pod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public struct Pod {

// Amount of insulin delivered for priming
public static let primeUnits = 2.6

// Default and limits for expiration reminder alerts
public static let expirationReminderAlertDefaultTimeBeforeExpiration = TimeInterval.hours(2)
public static let expirationReminderAlertMinTimeBeforeExpiration = TimeInterval.hours(1)
public static let expirationReminderAlertMaxTimeBeforeExpiration = TimeInterval.hours(24)
}

public enum SetupState: UInt8 {
Expand Down
78 changes: 73 additions & 5 deletions OmniKit/PumpManager/OmnipodPumpManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import HealthKit
import LoopKit
import RileyLinkKit
import RileyLinkBLEKit
import UserNotifications
import os.log

public enum ReservoirAlertState {
Expand Down Expand Up @@ -247,6 +248,17 @@ public class OmnipodPumpManager: RileyLinkPumpManager, PumpManager {
}
}
}

public var expirationReminderDate: Date? {
set {
self.state.expirationReminderDate = newValue
clearPodExpirationNotification()
schedulePodExpirationNotification()
}
get {
return self.state.expirationReminderDate
}
}

private var device: HKDevice {
if let podState = state.podState {
Expand Down Expand Up @@ -344,7 +356,15 @@ public class OmnipodPumpManager: RileyLinkPumpManager, PumpManager {
return self.state.podState?.isActive == true
}

public weak var pumpManagerDelegate: PumpManagerDelegate?
public weak var pumpManagerDelegate: PumpManagerDelegate? {
didSet {
pumpManagerDelegate?.clearNotification(for: self, identifier: LoopNotificationCategory.pumpExpirationWarning.rawValue)
self.queue.async {
self.clearPodExpirationNotification()
self.schedulePodExpirationNotification()
}
}
}

public let log = OSLog(category: "OmnipodPumpManager")

Expand Down Expand Up @@ -372,7 +392,48 @@ public class OmnipodPumpManager: RileyLinkPumpManager, PumpManager {

return lines.joined(separator: "\n")
}




// MARK: - Notifications

static let podExpirationNotificationIdentifier = "Omnipod:\(LoopNotificationCategory.pumpExpired.rawValue)"

func schedulePodExpirationNotification() {

if let expirationReminderDate = self.state.expirationReminderDate, expirationReminderDate.timeIntervalSinceNow > 0, let expiresAt = self.state.podState?.expiresAt {

let content = UNMutableNotificationContent()

let timeBetweenNoticeAndExpiration = expiresAt.timeIntervalSince(expirationReminderDate)

let formatter = DateComponentsFormatter()
formatter.maximumUnitCount = 1
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .full

let timeUntilExpiration = formatter.string(from: timeBetweenNoticeAndExpiration) ?? ""

content.title = NSLocalizedString("Pod Expiration Notice", comment: "The title for pod expiration notification")

content.body = String(format: NSLocalizedString("Time to replace your pod! Your pod will expire in %1$@", comment: "The format string for pod expiration notification body (1: time until expiration)"), timeUntilExpiration)
content.sound = UNNotificationSound.default
content.categoryIdentifier = LoopNotificationCategory.pumpExpired.rawValue
content.threadIdentifier = LoopNotificationCategory.pumpExpired.rawValue

let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: expirationReminderDate.timeIntervalSinceNow,
repeats: false
)

self.pumpManagerDelegate?.scheduleNotification(for: self, identifier: OmnipodPumpManager.podExpirationNotificationIdentifier, content: content, trigger: trigger)
}
}

func clearPodExpirationNotification() {
self.pumpManagerDelegate?.clearNotification(for: self, identifier: OmnipodPumpManager.podExpirationNotificationIdentifier)
}

// MARK: - Pod comms
private(set) var podComms: PodComms

Expand Down Expand Up @@ -411,9 +472,10 @@ public class OmnipodPumpManager: RileyLinkPumpManager, PumpManager {
// MARK: Testing
private func jumpStartPod(address: UInt32, lot: UInt32, tid: UInt32, fault: PodInfoFaultEvent? = nil, startDate: Date? = nil, mockFault: Bool) {
let start = startDate ?? Date()
let expire = start.addingTimeInterval(.days(3))
self.state.podState = PodState(address: address, activatedAt: start, expiresAt: expire, piVersion: "jumpstarted", pmVersion: "jumpstarted", lot: lot, tid: tid)
self.state.podState = PodState(address: address, piVersion: "jumpstarted", pmVersion: "jumpstarted", lot: lot, tid: tid)
self.state.podState?.setupProgress = .podConfigured
self.state.podState?.activatedAt = start
self.state.expirationReminderDate = start + .hours(70)

let fault = mockFault ? try? PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020d0000000e00c36a020703ff020900002899080082")!) : nil
self.state.podState?.fault = fault
Expand Down Expand Up @@ -447,7 +509,7 @@ public class OmnipodPumpManager: RileyLinkPumpManager, PumpManager {
self.state.unstoredDoses.removeAll()
}
}

if let podState = self.state.podState, !podState.setupProgress.primingNeeded {
completion(.failure(OmnipodPumpManagerError.podAlreadyPrimed))
return
Expand All @@ -473,12 +535,18 @@ public class OmnipodPumpManager: RileyLinkPumpManager, PumpManager {
completion(.failure(pairError))
return
}

guard let podState = self.state.podState else {
completion(.failure(OmnipodPumpManagerError.noPodPaired))
return
}

self.podComms.runSession(withName: "Configure and prime pod", using: deviceSelector) { (result) in
switch result {
case .success(let session):
do {
let primeFinishedAt = try session.prime()
self.state.expirationReminderDate = podState.expiresAt?.addingTimeInterval(-Pod.expirationReminderAlertDefaultTimeBeforeExpiration)
completion(.success(primeFinishedAt))
} catch let error {
completion(.failure(error))
Expand Down
13 changes: 12 additions & 1 deletion OmniKit/PumpManager/OmnipodPumpManagerState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {

public var unstoredDoses: [UnfinalizedDose]

public var expirationReminderDate: Date?

public init(podState: PodState?, timeZone: TimeZone, basalSchedule: BasalSchedule, rileyLinkConnectionManagerState: RileyLinkConnectionManagerState?) {
self.podState = podState
self.timeZone = timeZone
Expand Down Expand Up @@ -69,7 +71,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
} else {
podState = nil
}

let timeZone: TimeZone
if let timeZoneSeconds = rawValue["timeZone"] as? Int,
let tz = TimeZone(secondsFromGMT: timeZoneSeconds) {
Expand All @@ -95,6 +97,10 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
if let rawMessageLog = rawValue["messageLog"] as? MessageLog.RawValue, let messageLog = MessageLog(rawValue: rawMessageLog) {
self.messageLog = messageLog
}

if let expirationReminderDate = rawValue["expirationReminderDate"] as? Date {
self.expirationReminderDate = expirationReminderDate
}
}

public var rawValue: RawValue {
Expand All @@ -108,6 +114,10 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
if let podState = podState {
value["podState"] = podState.rawValue
}

if let expirationReminderDate = expirationReminderDate {
value["expirationReminderDate"] = expirationReminderDate
}

if let rileyLinkConnectionManagerState = rileyLinkConnectionManagerState {
value["rileyLinkConnectionManagerState"] = rileyLinkConnectionManagerState.rawValue
Expand All @@ -128,6 +138,7 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible {
return [
"* timeZone: \(timeZone)",
"* basalSchedule: \(String(describing: basalSchedule))",
"* expirationReminderDate: \(String(describing: expirationReminderDate))",
String(reflecting: podState),
String(reflecting: rileyLinkConnectionManagerState),
String(reflecting: messageLog),
Expand Down
8 changes: 1 addition & 7 deletions OmniKit/PumpManager/PodComms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,9 @@ class PodComms : CustomDebugStringConvertible {
throw PodCommsError.unexpectedResponse(response: responseType)
}

let activationDate = Date()

let expirationDate = activationDate + Pod.serviceDuration - Pod.endOfServiceImminentWindow - Pod.expirationAdvisoryWindow

// Pairing state should be addressAssigned
self.podState = PodState(
address: address,
activatedAt: activationDate,
expiresAt: expirationDate,
piVersion: String(describing: config.piVersion),
pmVersion: String(describing: config.pmVersion),
lot: config.lot,
Expand All @@ -115,7 +109,7 @@ class PodComms : CustomDebugStringConvertible {
let transport = PodMessageTransport(session: commandSession, address: 0xffffffff, ackAddress: podState.address, state: podState.messageTransportState)
transport.messageLogger = messageLogger

let dateComponents = ConfigurePodCommand.dateComponents(date: podState.activatedAt, timeZone: timeZone)
let dateComponents = ConfigurePodCommand.dateComponents(date: Date(), timeZone: timeZone)
let setupPod = ConfigurePodCommand(address: podState.address, dateComponents: dateComponents, lot: podState.lot, tid: podState.tid)

let message = Message(address: 0xffffffff, messageBlocks: [setupPod], sequenceNum: transport.messageNumber)
Expand Down
22 changes: 14 additions & 8 deletions OmniKit/PumpManager/PodCommsSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,9 @@ public class PodCommsSession {
// The following will set Tab5[$16] to 0 during pairing, which disables $6x faults.
let _: StatusResponse = try send([FaultConfigCommand(nonce: podState.currentNonce, tab5Sub16: 0, tab5Sub17: 0)])

let lowReservoirAlarm = PodAlert.lowReservoirAlarm(20) // Alarm at 20 units remaining
let _ = try configureAlerts([lowReservoirAlarm])
// Uncomment to get an audible pod alert for low reservoir
// let lowReservoirAlarm = PodAlert.lowReservoirAlarm(20) // Alarm at 20 units remaining
// let _ = try configureAlerts([lowReservoirAlarm])

let finishSetupReminder = PodAlert.finishSetupReminder
let _ = try configureAlerts([finishSetupReminder])
Expand Down Expand Up @@ -291,18 +292,19 @@ public class PodCommsSession {
}

public func programInitialBasalSchedule(_ basalSchedule: BasalSchedule, scheduleOffset: TimeInterval) throws {
if podState.setupProgress != .settingInitialBasalSchedule {
let timeUntilExpirationAlert = (podState.activatedAt + Pod.serviceDuration - Pod.endOfServiceImminentWindow - Pod.expirationAdvisoryWindow - Pod.expirationAlertWindow).timeIntervalSinceNow
let expirationAlert = PodAlert.expirationAlert(timeUntilExpirationAlert)
let _ = try configureAlerts([expirationAlert])
} else {
if podState.setupProgress == .settingInitialBasalSchedule {
// We started basal schedule programming, but didn't get confirmation somehow, so check status
let status: StatusResponse = try send([GetStatusCommand()])
podState.updateFromStatusResponse(status)
if status.podProgressStatus == .readyForCannulaInsertion {
podState.setupProgress = .initialBasalScheduleSet
return
}
} else {
// Uncomment the following to get an audible expiration notice before the expiration advisory alert
// let timeUntilExpirationAlert = (podState.activatedAt + Pod.serviceDuration - Pod.endOfServiceImminentWindow - Pod.expirationAdvisoryWindow - Pod.expirationAlertWindow).timeIntervalSinceNow
// let expirationAlert = PodAlert.expirationAlert(timeUntilExpirationAlert)
// let _ = try configureAlerts([expirationAlert])
}

podState.setupProgress = .settingInitialBasalSchedule
Expand All @@ -326,6 +328,10 @@ public class PodCommsSession {
public func insertCannula() throws -> TimeInterval {
let insertionWait: TimeInterval = .seconds(10)

guard let activatedAt = podState.activatedAt else {
throw PodCommsError.noPodPaired
}

if podState.setupProgress == .startingInsertCannula || podState.setupProgress == .cannulaInserting {
// We started cannula insertion, but didn't get confirmation somehow, so check status
let status: StatusResponse = try send([GetStatusCommand()])
Expand All @@ -340,7 +346,7 @@ public class PodCommsSession {
}
} else {
// Configure Alerts
let endOfServiceTime = podState.activatedAt + Pod.serviceDuration
let endOfServiceTime = activatedAt + Pod.serviceDuration
let timeUntilExpirationAdvisory = (endOfServiceTime - Pod.endOfServiceImminentWindow - Pod.expirationAdvisoryWindow).timeIntervalSinceNow
let expirationAdvisoryAlarm = PodAlert.expirationAdvisoryAlarm(alarmTime: timeUntilExpirationAdvisory, duration: Pod.expirationAdvisoryWindow)
let shutdownImminentAlarm = PodAlert.shutdownImminentAlarm((endOfServiceTime - Pod.endOfServiceImminentWindow).timeIntervalSinceNow)
Expand Down
20 changes: 11 additions & 9 deletions OmniKit/PumpManager/PodState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl

public let address: UInt32
fileprivate var nonceState: NonceState
public let activatedAt: Date
public let expiresAt: Date
public var activatedAt: Date?

public var expiresAt: Date? {
return activatedAt?.addingTimeInterval(Pod.serviceDuration - Pod.endOfServiceImminentWindow - Pod.expirationAdvisoryWindow)
}

public let piVersion: String
public let pmVersion: String
public let lot: UInt32
Expand Down Expand Up @@ -82,11 +86,9 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
return active
}

public init(address: UInt32, activatedAt: Date, expiresAt: Date, piVersion: String, pmVersion: String, lot: UInt32, tid: UInt32) {
public init(address: UInt32, piVersion: String, pmVersion: String, lot: UInt32, tid: UInt32) {
self.address = address
self.nonceState = NonceState(lot: lot, tid: tid)
self.activatedAt = activatedAt
self.expiresAt = expiresAt
self.piVersion = piVersion
self.pmVersion = pmVersion
self.lot = lot
Expand Down Expand Up @@ -214,7 +216,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
let nonceStateRaw = rawValue["nonceState"] as? NonceState.RawValue,
let nonceState = NonceState(rawValue: nonceStateRaw),
let activatedAt = rawValue["activatedAt"] as? Date,
let expiresAt = rawValue["expiresAt"] as? Date,
let piVersion = rawValue["piVersion"] as? String,
let pmVersion = rawValue["pmVersion"] as? String,
let lot = rawValue["lot"] as? UInt32,
Expand All @@ -226,7 +227,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
self.address = address
self.nonceState = nonceState
self.activatedAt = activatedAt
self.expiresAt = expiresAt
self.piVersion = piVersion
self.pmVersion = pmVersion
self.lot = lot
Expand Down Expand Up @@ -336,8 +336,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
var rawValue: RawValue = [
"address": address,
"nonceState": nonceState.rawValue,
"activatedAt": activatedAt,
"expiresAt": expiresAt,
"piVersion": piVersion,
"pmVersion": pmVersion,
"lot": lot,
Expand Down Expand Up @@ -377,6 +375,10 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
rawValue["primeFinishTime"] = primeFinishTime
}

if let activatedAt = activatedAt {
rawValue["activatedAt"] = activatedAt
}

if configuredAlerts.count > 0 {
let rawConfiguredAlerts = Dictionary(uniqueKeysWithValues:
configuredAlerts.map { slot, alarm in (String(describing: slot.rawValue), alarm.rawValue) })
Expand Down
Loading

0 comments on commit 97acf9d

Please sign in to comment.