344 lines
18 KiB
Swift
344 lines
18 KiB
Swift
import Foundation
|
|
|
|
/// low, high, very low, very high, ...
|
|
public enum AlertKind:Int, CaseIterable {
|
|
// when adding alertkinds, try to add new cases at the end (ie 7, ...)
|
|
// if this is done in the middle ((eg rapid rise alert might seem better positioned after veryhigh), then a database migration would be required, because the rawvalue is stored as Int16 in the coredata, namely the alertkind
|
|
// the order of the alerts will also be the order in the settings
|
|
|
|
case verylow = 0
|
|
case low = 1
|
|
case high = 2
|
|
case veryhigh = 3
|
|
case missedreading = 4
|
|
case calibration = 5
|
|
case batterylow = 6
|
|
|
|
/// example, low alert needs a value = value below which alert needs to fire - there's actually no alert right now that doesn't need a value, in iosxdrip there was the iphonemuted alert, but I removed this here. Function remains, never now it might come back
|
|
///
|
|
/// probably only useful in UI - named AlertKind and not AlertType because there's already an AlertType which has a different goal
|
|
func needsAlertValue() -> Bool {
|
|
switch self {
|
|
case .low, .high, .verylow,.veryhigh,.missedreading,.calibration,.batterylow:
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// if value is a bg value, the conversion to mmol will be needed
|
|
///
|
|
/// will only be useful in UI
|
|
func valueNeedsConversionToMmol() -> Bool {
|
|
switch self {
|
|
|
|
case .low, .high, .verylow, .veryhigh:
|
|
return true
|
|
case .missedreading, .calibration, .batterylow:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// at initial startup, a default alertentry will be created for every kind of alert. This function defines the default value to be used
|
|
func defaultAlertValue() -> Int {
|
|
switch self {
|
|
|
|
case .low:
|
|
return ConstantsDefaultAlertLevels.low
|
|
case .high:
|
|
return ConstantsDefaultAlertLevels.high
|
|
case .verylow:
|
|
return ConstantsDefaultAlertLevels.veryLow
|
|
case .veryhigh:
|
|
return ConstantsDefaultAlertLevels.veryHigh
|
|
case .missedreading:
|
|
return ConstantsDefaultAlertLevels.missedReading
|
|
case .calibration:
|
|
return ConstantsDefaultAlertLevels.calibration
|
|
case .batterylow:
|
|
if let transmitterType = UserDefaults.standard.transmitterType {
|
|
return transmitterType.defaultBatteryAlertLevel()
|
|
} else {
|
|
return ConstantsDefaultAlertLevels.defaultBatteryAlertLevelMiaoMiao
|
|
}
|
|
}
|
|
}
|
|
|
|
/// description of the alert to be used for logging
|
|
func descriptionForLogging() -> String {
|
|
switch self {
|
|
|
|
case .low:
|
|
return "low"
|
|
case .high:
|
|
return "high"
|
|
case .verylow:
|
|
return "verylow"
|
|
case .veryhigh:
|
|
return "veryhigh"
|
|
case .missedreading:
|
|
return "missedreading"
|
|
case .calibration:
|
|
return "calibration"
|
|
case .batterylow:
|
|
return "batterylow"
|
|
}
|
|
}
|
|
|
|
/// verify if alert needs to be fired or not.
|
|
///
|
|
/// The caller of this function must have checked already checked that lastBgReading is recent and that it has a running sensor - and that calibration is also for the last sensor
|
|
///
|
|
/// Not every input parameter will be used, depending on the alertKind. For example, alertKind .calibration will not use the lastBgReading, it will use the lastCalibration
|
|
///
|
|
/// - parameters:
|
|
/// - currentAlertEntry : the currently applicable AlertEntry, meaning for the actual time of the day
|
|
/// - nextAlertEntry : the next applicable AlertEntry, ie the one that comes after currentAlertEntry
|
|
/// - lastBgReading : should be reading for the currently active sensor with calculated value != 0
|
|
/// - lastButOneBgReading : should als be for the currently active sensor with calculated value != 0, it is only there to be able to calculate the unitizedDeltaString for the alertBody
|
|
/// - lastCalibration : is to allow to raise a calibration alert
|
|
/// - transmitterBatteryInfo : is to allow to raise a battery level alert
|
|
/// - returns:
|
|
/// - bool : If the bool is false, then there's no need to raise an alert.
|
|
/// - alertbody : AlertBody, AlertTitle and delay are used if an alert needs to be raised for the notification.
|
|
/// - alerttitle : AlertBody, AlertTitle and delay are used if an alert needs to be raised for the notification.
|
|
/// - delayInSeconds : If delayInSeconds not nil and > 0 or if delayInSeconds is nil, then the alert will be a future planned Alert. This will only be applicable to missed reading alerts.
|
|
func alertNeeded(currentAlertEntry:AlertEntry, nextAlertEntry:AlertEntry?, lastBgReading:BgReading?, _ lastButOneBgReading:BgReading?, lastCalibration:Calibration?, transmitterBatteryInfo:TransmitterBatteryInfo?) -> (alertNeeded:Bool, alertBody:String?, alertTitle:String?, delayInSeconds:Int?) {
|
|
//Not all input parameters in the closure are needed for every type of alert. - this is to make it generic
|
|
switch self {
|
|
|
|
case .low,.verylow:
|
|
// if alertEntry not enabled, return false
|
|
if !currentAlertEntry.alertType.enabled {return (false, nil, nil, nil)}
|
|
|
|
if let lastBgReading = lastBgReading {
|
|
// first check if lastBgReading not nil and calculatedValue > 0.0, never know that it's not been checked by caller
|
|
if lastBgReading.calculatedValue == 0.0 {return (false, nil, nil, nil)}
|
|
// now do the actual check if alert is applicable or not
|
|
if lastBgReading.calculatedValue < Double(currentAlertEntry.value) {
|
|
return (true, lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl), createAlertTitleForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), nil)
|
|
} else {return (false, nil, nil, nil)}
|
|
} else {return (false, nil, nil, nil)}
|
|
|
|
case .high,.veryhigh:
|
|
// if alertEntry not enabled, return false
|
|
if !currentAlertEntry.alertType.enabled {return (false, nil, nil, nil)}
|
|
|
|
if let lastBgReading = lastBgReading {
|
|
// first check if calculatedValue > 0.0, never know that it's not been checked by caller
|
|
if lastBgReading.calculatedValue == 0.0 {return (false, nil, nil, nil)}
|
|
// now do the actual check if alert is applicable or not
|
|
if lastBgReading.calculatedValue > Double(currentAlertEntry.value) {
|
|
return (true, lastBgReading.unitizedDeltaString(previousBgReading: lastButOneBgReading, showUnit: true, highGranularity: true, mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl), createAlertTitleForBgReadingAlerts(bgReading: lastBgReading, alertKind: self), nil)
|
|
} else {return (false, nil, nil, nil)}
|
|
} else {return (false, nil, nil, nil)}
|
|
|
|
case .missedreading:
|
|
// if no valid lastbgreading then there's definitely no need to plan an alert
|
|
guard let lastBgReading = lastBgReading else {return (false, nil, nil, nil)}
|
|
|
|
// this will be the delay of the planned notification, in seconds
|
|
var delayToUseInSeconds:Int?
|
|
|
|
// calculate time since last reading in minutes
|
|
let timeSinceLastReadingInMinutes:Int = Int((Date().toMillisecondsAsDouble() - lastBgReading.timeStamp.toMillisecondsAsDouble())/1000/60)
|
|
|
|
// first check if currentalertEntry has an enabled alerttype
|
|
if currentAlertEntry.alertType.enabled {
|
|
|
|
// delay to use in the alert is value in the alertEntry - time since last reading in minutes
|
|
delayToUseInSeconds = (Int(currentAlertEntry.value) - timeSinceLastReadingInMinutes) * 60
|
|
|
|
// check now if there's a next alert entry , and if so, check if the alert time would be in the time period of that next alert, and if it's not enabled, if so then no alert will not be scheduled
|
|
if let nextAlertEntry = nextAlertEntry {
|
|
|
|
// if start of nextAlertEntry < start of currentAlertEntry, then ad 24 hours, because it means the nextAlertEntry is actually the one of the day after
|
|
var nextAlertEntryStartValueToUse = nextAlertEntry.start
|
|
if nextAlertEntry.start < currentAlertEntry.start {
|
|
nextAlertEntryStartValueToUse += nextAlertEntryStartValueToUse + 24 * 60
|
|
}
|
|
|
|
if !nextAlertEntry.alertType.enabled {
|
|
|
|
// calculate when alert would fire and check if >= nextAlertEntry.start , if so don't plan an alert
|
|
if Date().minutesSinceMidNightLocalTime() + delayToUseInSeconds!/60 >= nextAlertEntryStartValueToUse {
|
|
// no need to plan a missed reading alert
|
|
return (false, nil, nil, nil)
|
|
}
|
|
|
|
} else {
|
|
// next alertentry is enabled, maybe the missed reading alert value is higher
|
|
if nextAlertEntry.value > currentAlertEntry.value && Date().minutesSinceMidNightLocalTime() + delayToUseInSeconds!/60 > nextAlertEntryStartValueToUse {
|
|
delayToUseInSeconds = (Int(nextAlertEntry.value) - timeSinceLastReadingInMinutes) * 60
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// there's no nextAlertEntry, use the already calculated value for delayToUseInSeconds based on currentAlertEntry
|
|
return (true, "", Texts_Alerts.missedReadingAlertTitle, delayToUseInSeconds)
|
|
|
|
} else {
|
|
|
|
// current alertEntry is not enabled but maybe the next one is and it's enabled
|
|
if let nextAlertEntry = nextAlertEntry, nextAlertEntry.alertType.enabled {
|
|
|
|
// earliest expiry of alert should be time that nextAlertEntry is valid
|
|
// if the diff between that time and time of latestreading is less than nextAlertEntry.value, then we set actual delay to nextAlertEntry.value
|
|
|
|
// start with maximum value
|
|
delayToUseInSeconds = (Int(nextAlertEntry.value) - timeSinceLastReadingInMinutes) * 60 // usually timeSinceLastReadingInMinutes will be 0 because this code is executed immediately after having received a reading
|
|
|
|
// if start of nextAlertEntry < start of currentAlertEntry, then ad 24 hours, because it means the nextAlertEntry is actually the one of the day after
|
|
var nextAlertEntryStartValueToUse = nextAlertEntry.start
|
|
if nextAlertEntry.start < currentAlertEntry.start {
|
|
nextAlertEntryStartValueToUse += nextAlertEntryStartValueToUse + 24 * 60
|
|
}
|
|
|
|
// if this would be before start of nextAlertEntry then increase the delay
|
|
var minutesSinceMidnightOfExpirtyTime = Date(timeInterval: TimeInterval(delayToUseInSeconds!), since: lastBgReading.timeStamp).minutesSinceMidNightLocalTime()
|
|
if minutesSinceMidnightOfExpirtyTime < Date().minutesSinceMidNightLocalTime() {
|
|
minutesSinceMidnightOfExpirtyTime += 24 * 60
|
|
}
|
|
let diffInMinutes = Int(nextAlertEntryStartValueToUse) - minutesSinceMidnightOfExpirtyTime
|
|
if diffInMinutes > 0 {
|
|
delayToUseInSeconds = delayToUseInSeconds! + diffInMinutes * 60
|
|
}
|
|
|
|
return (true, "", Texts_Alerts.missedReadingAlertTitle, delayToUseInSeconds)
|
|
|
|
} else {
|
|
// none of alertentries enables missed reading, nothing to plan
|
|
return (false, nil, nil, nil)
|
|
}
|
|
}
|
|
|
|
case .calibration:
|
|
// if alertEntry not enabled, return false
|
|
// if lastCalibration == nil then also no need to create an alert, could be an oop web enabled transmitter
|
|
if !currentAlertEntry.alertType.enabled || lastCalibration == nil {return (false, nil, nil, nil)}
|
|
|
|
// if lastCalibration not nil, check the timestamp and check if delay > value (in hours)
|
|
if abs(lastCalibration!.timeStamp.timeIntervalSinceNow) > TimeInterval(Int(currentAlertEntry.value) * 3600) {
|
|
return(true, "", Texts_Alerts.calibrationNeededAlertTitle, nil)
|
|
}
|
|
return (false, nil, nil, nil)
|
|
|
|
case .batterylow:
|
|
// if alertEntry not enabled, return false
|
|
if !currentAlertEntry.alertType.enabled {return (false, nil, nil, nil)}
|
|
|
|
// if transmitterBatteryInfo is nil, return false
|
|
guard let transmitterBatteryInfo = transmitterBatteryInfo else {return (false, nil, nil, nil)}
|
|
|
|
// get level
|
|
var batteryLevelToCheck:Int?
|
|
|
|
switch transmitterBatteryInfo {
|
|
case .percentage(let percentage):
|
|
batteryLevelToCheck = percentage
|
|
case .DexcomG5(let voltageA, _, _, _, _):
|
|
batteryLevelToCheck = voltageA
|
|
case .DexcomG4(let level):
|
|
batteryLevelToCheck = level
|
|
}
|
|
|
|
if let batteryLevelToCheck = batteryLevelToCheck, currentAlertEntry.value > batteryLevelToCheck {
|
|
return (true, "", Texts_Alerts.batteryLowAlertTitle, nil)
|
|
}
|
|
|
|
return (false, nil, nil, nil)
|
|
}
|
|
}
|
|
|
|
/// returns notification identifier for local notifications, for specific alertKind.
|
|
func notificationIdentifier() -> String {
|
|
switch self {
|
|
|
|
case .low:
|
|
return ConstantsNotifications.NotificationIdentifiersForAlerts.lowAlert
|
|
case .high:
|
|
return ConstantsNotifications.NotificationIdentifiersForAlerts.highAlert
|
|
case .verylow:
|
|
return ConstantsNotifications.NotificationIdentifiersForAlerts.veryLowAlert
|
|
case .veryhigh:
|
|
return ConstantsNotifications.NotificationIdentifiersForAlerts.veryHighAlert
|
|
case .missedreading:
|
|
return ConstantsNotifications.NotificationIdentifiersForAlerts.missedReadingAlert
|
|
case .calibration:
|
|
return ConstantsNotifications.NotificationIdentifiersForCalibration.subsequentCalibrationRequest
|
|
case .batterylow:
|
|
return ConstantsNotifications.NotificationIdentifiersForAlerts.batteryLow
|
|
}
|
|
}
|
|
|
|
/// to be used in when name of alert needs be shown, eg pickerview, or in list of alert setings
|
|
func alertTitle() -> String {
|
|
switch self {
|
|
|
|
case .low:
|
|
return Texts_Alerts.lowAlertTitle
|
|
case .high:
|
|
return Texts_Alerts.highAlertTitle
|
|
case .verylow:
|
|
return Texts_Alerts.veryLowAlertTitle
|
|
case .veryhigh:
|
|
return Texts_Alerts.veryHighAlertTitle
|
|
case .missedreading:
|
|
return Texts_Alerts.missedReadingAlertTitle
|
|
case .calibration:
|
|
return Texts_Alerts.calibrationNeededAlertTitle
|
|
case .batterylow:
|
|
return Texts_Alerts.batteryLowAlertTitle
|
|
}
|
|
}
|
|
|
|
/// for UI, when value is requested, text should show also the unit (eg mgdl, mmol, minutes, days ...)
|
|
/// What is this text ?
|
|
func valueUnitText(transmitterType:CGMTransmitterType?) -> String {
|
|
switch self {
|
|
|
|
case .verylow, .low, .high, .veryhigh:
|
|
return UserDefaults.standard.bloodGlucoseUnitIsMgDl ? Texts_Common.mgdl:Texts_Common.mmol
|
|
case .missedreading:
|
|
return Texts_Common.minutes
|
|
case .calibration:
|
|
return Texts_Common.hours
|
|
case .batterylow:
|
|
if let transmitterType = transmitterType {
|
|
return transmitterType.batteryUnit()
|
|
} else {
|
|
return ""// even though 20 is used as default alert level (assuming 20%) give as default value empty string
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// specifically for high, low, very high, very low because these need the same kind of alertTitle
|
|
fileprivate func createAlertTitleForBgReadingAlerts(bgReading:BgReading, alertKind:AlertKind) -> String {
|
|
var returnValue:String = ""
|
|
|
|
// the start of the body, which says like "High Alert"
|
|
switch alertKind {
|
|
|
|
case .low:
|
|
returnValue = returnValue + Texts_Alerts.lowAlertTitle
|
|
case .high:
|
|
returnValue = returnValue + Texts_Alerts.highAlertTitle
|
|
case .verylow:
|
|
returnValue = returnValue + Texts_Alerts.veryLowAlertTitle
|
|
case .veryhigh:
|
|
returnValue = returnValue + Texts_Alerts.veryHighAlertTitle
|
|
default:
|
|
return returnValue
|
|
}
|
|
|
|
// add unit
|
|
returnValue = returnValue + " " + bgReading.calculatedValue.mgdlToMmolAndToString(mgdl: UserDefaults.standard.bloodGlucoseUnitIsMgDl)
|
|
|
|
// add slopeArrow
|
|
if !bgReading.hideSlope {
|
|
returnValue = returnValue + " " + bgReading.slopeArrow()
|
|
}
|
|
|
|
return returnValue
|
|
}
|