xdripswift/xdrip/Managers/Alerts/AlertKind.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
}