diff --git a/.gitignore b/.gitignore index 5917b40..5c13bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -#project Specific files -/LibreTransmitter/NotificationHelperOverride.swift - # OS X .DS_Store diff --git a/Common/Settings/GlucoseSchedules.swift b/Common/Settings/GlucoseSchedules.swift deleted file mode 100644 index 528d31a..0000000 --- a/Common/Settings/GlucoseSchedules.swift +++ /dev/null @@ -1,223 +0,0 @@ -// -// GlucoseSchedules.swift -// MiaomiaoClient -// -// Created by LoopKit Authors on 19/04/2019. -// Copyright © 2019 LoopKit Authors. All rights reserved. -// - -import Foundation -import HealthKit -// import MiaomiaoClient -public enum GlucoseScheduleAlarmResult: Int, CaseIterable { - case none = 0 - case low - case high - - func isAlarming() -> Bool { - rawValue != GlucoseScheduleAlarmResult.none.rawValue - } -} - -enum GlucoseSchedulesValidationStatus { - case success - case error(String) -} - -class GlucoseScheduleList: Codable, CustomStringConvertible { - var description: String { - "(schedules: \(schedules) )" - } - - public var schedules = [GlucoseSchedule]() - - public var enabledSchedules: [GlucoseSchedule] { - schedules.compactMap({ $0.enabled == true ? $0 : nil }) - } - - // this is only used by the ui to count total number of schedules - public static let minimumSchedulesCount = 2 - - public var activeSchedules: [GlucoseSchedule] { - enabledSchedules.compactMap { - if let activeTime = $0.getScheduleActiveToFrom() { - let now = Date() - return activeTime.contains(now) ? $0 : nil - } - return nil - } - } - - private func validateGlucoseThresholds() -> GlucoseSchedulesValidationStatus? { - // This is on purpose - // we check all chedules for valid thresholds - for schedule in self.schedules { - if let low = schedule.lowAlarm, let high = schedule.highAlarm { - if low == high { - return .error("One of your glucose schedules had the same value for low and high thresholds") - } - if low > high { - return .error("One of your glucose schedules had a low threshold set above your high threshold") - } - // just for completness sake, this would never be called - if high < low { - return .error("One of your glucose schedules had a high threshold set below your low threshold") - } - } - } - return nil - } - - public func validateGlucoseSchedules() -> GlucoseSchedulesValidationStatus { - if let errors = validateGlucoseThresholds() { - return errors - } - - // if we have zero or 1 enabled schedules, overlapping would not be possible - // (there is nothing to overlap on), so we skip interval check - guard self.enabledSchedules.count > 1 else { - return .success - } - - var sameStartEnd = false - let intervals: [DateInterval] = enabledSchedules.compactMap({ - var schedule = $0.getScheduleActiveToFrom() - if let start = schedule?.start, let end = schedule?.end { - if start == end { - sameStartEnd = true - return nil - } - } - // This compensates for Datetimes being closed range in nature - // example, - // interval1start = 12:00, interval1end=14:00 - // interval2start = 14:00, interval2end=24:00 - // interval1end and interval2 would collide when .intersect()-ing, - // so we change interval1end to 13:59:59 - // and interval2end to 23:59:59 - // This function is only used in the gui for validation, so this is acceptable - // - if let end = schedule?.end { - if let newEnd = Calendar.current.date(byAdding: .second, value: -1, to: end) { - schedule?.end = newEnd - } - } - return schedule - }) - if sameStartEnd { - return .error("One interval had the same start and end!") - } - if let intersects = intervals.intersect() { - print("Glucose schedule collided, not valid! \(intersects)") - return .error("Glucose schedules had overlapping time intervals") - } - return .success - } - // for convenience - public static var snoozedUntil: Date? { - UserDefaults.standard.snoozedUntil - } - - public static func isSnoozed() -> Bool { - let now = Date() - - if let snoozedUntil { - return snoozedUntil >= now - } - return false - } - - public func getActiveAlarms(_ currentGlucoseInMGDL: Double) -> GlucoseScheduleAlarmResult { - for schedule in self.activeSchedules { - if let lowAlarm = schedule.lowAlarm, currentGlucoseInMGDL <= lowAlarm { - return .low - } - if let highAlarm = schedule.highAlarm, currentGlucoseInMGDL >= highAlarm { - return .high - } - } - return .none - } -} - -class GlucoseSchedule: Codable, CustomStringConvertible { - var from: DateComponents? - var to: DateComponents? - var lowAlarm: Double? - var highAlarm: Double? - var enabled: Bool? - - // glucose schedules are stored as standalone datecomponents (i.e. offsets) - // this takes the current start of day and adds those offsets, - // and returns a Dateinterval with those offsets applied - public func getScheduleActiveToFrom() -> DateInterval? { - guard let fromComponents = from, let toComponents = to else { - return nil - } - - let now = Date() - let previousMidnight = Calendar.current.startOfDay(for: now) - let helper = Calendar.current.date(byAdding: .day, value: 1, to: previousMidnight)! - let nextMidnight = Calendar.current.startOfDay(for: helper) - - let fromDate: Date? = Calendar.current.date(byAdding: fromComponents, to: previousMidnight) - var toDate: Date? - if toComponents.minute == 0 && toComponents.hour == 0 { - toDate = nextMidnight - } else { - toDate = Calendar.current.date(byAdding: toComponents, to: previousMidnight)! - } - - if let fromDate, let toDate, toDate >= fromDate { - return DateInterval(start: fromDate, end: toDate) - } - return nil - } - // stores the alarm. It does not synhronize the value with the underlaying userdefaults - // that is up to the caller of this class - public func storeLowAlarm(forUnit unit: HKUnit, lowAlarm: Double) { - if unit == HKUnit.millimolesPerLiter { - self.lowAlarm = lowAlarm * 18 - return - } - - self.lowAlarm = lowAlarm - } - - public func retrieveLowAlarm(forUnit unit: HKUnit) -> Double? { - if let lowAlarm = self.lowAlarm { - if unit == HKUnit.millimolesPerLiter { - return (lowAlarm / 18).roundTo(places: 1) - } else { - return lowAlarm - } - } - - return nil - } - - // stores the alarm. It does not synhronize the value with the underlaying userdefaults - // that is up to the caller of this class - public func storeHighAlarm(forUnit unit: HKUnit, highAlarm: Double) { - if unit == HKUnit.millimolesPerLiter { - self.highAlarm = highAlarm * 18 - return - } - - self.highAlarm = highAlarm - } - public func retrieveHighAlarm(forUnit unit: HKUnit) -> Double? { - if let highAlarm = self.highAlarm { - if unit == HKUnit.millimolesPerLiter { - return (highAlarm / 18).roundTo(places: 1) - } - return highAlarm - } - - return nil - } - - var description: String { - "(from: \(String(describing: from)), to: \(String(describing: to)), low: \(String(describing: lowAlarm)), high: \(String(describing: highAlarm)), enabled: \(String(describing: enabled)))" - } -} diff --git a/Common/Settings/UserDefaults+Alarmsettings.swift b/Common/Settings/UserDefaults+Alarmsettings.swift deleted file mode 100644 index a05b78e..0000000 --- a/Common/Settings/UserDefaults+Alarmsettings.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// Userdefaults+Alarmsettings.swift -// MiaomiaoClient -// -// Created by LoopKit Authors on 20/04/2019. -// Copyright © 2019 LoopKit Authors. All rights reserved. -// - -import Foundation -import HealthKit - -extension UserDefaults { - private enum Key: String { - case glucoseSchedules = "com.loopkit.libreglucoseschedules" - - case mmAlwaysDisplayGlucose = "com.loopkit.libreAlwaysDisplayGlucose" - case mmNotifyEveryXTimes = "com.loopkit.libreNotifyEveryXTimes" - - case mmAlertLowBatteryWarning = "com.loopkit.libreLowBatteryWarning" - case mmAlertInvalidSensorDetected = "com.loopkit.libreInvalidSensorDetected" - // case mmAlertalarmNotifications - case mmAlertNewSensorDetected = "com.loopkit.libreNewSensorDetected" - case mmAlertNoSensorDetected = "com.loopkit.libreNoSensorDetected" - case mmGlucoseUnit = "com.loopkit.libreGlucoseUnit" - case mmAlertSensorSoonExpire = "com.loopkit.libreAlertSensorSoonExpire" - case mmSnoozedUntil = "com.loopkit.libreSnoozedUntil" - case mmDangerMode = "com.loopkit.libreDangerModeActivated" - case mmShowPhoneBattery = "com.loopkit.libreShowPhoneBattery" - case mmShowTransmitterBattery = "com.loopkit.libreShowTransmitterBattery" - case mmCriticalAlarmsVolume = "com.loopkit.libreCriticalAlarmsVolume" - } - - public func optionalBool(forKey defaultName: String) -> Bool? { - if let value = value(forKey: defaultName) { - return value as? Bool - } - return nil - } - - var mmShowPhoneBattery: Bool { - get { - optionalBool(forKey: Key.mmShowPhoneBattery.rawValue) ?? false - } - set { - set(newValue, forKey: Key.mmShowPhoneBattery.rawValue) - } - } - - var mmAlwaysDisplayGlucose: Bool { - get { - optionalBool(forKey: Key.mmAlwaysDisplayGlucose.rawValue) ?? true - } - set { - set(newValue, forKey: Key.mmAlwaysDisplayGlucose.rawValue) - } - } - var mmNotifyEveryXTimes: Int { - get { - integer(forKey: Key.mmNotifyEveryXTimes.rawValue) - } - set { - set(newValue, forKey: Key.mmNotifyEveryXTimes.rawValue) - } - } - - var mmAlertLowBatteryWarning: Bool { - get { - optionalBool(forKey: Key.mmAlertLowBatteryWarning.rawValue) ?? true - } - set { - set(newValue, forKey: Key.mmAlertLowBatteryWarning.rawValue) - } - } - var mmAlertInvalidSensorDetected: Bool { - get { - optionalBool(forKey: Key.mmAlertInvalidSensorDetected.rawValue) ?? true - } - set { - set(newValue, forKey: Key.mmAlertInvalidSensorDetected.rawValue) - } - } - - var mmAlertNewSensorDetected: Bool { - get { - optionalBool(forKey: Key.mmAlertNewSensorDetected.rawValue) ?? true - } - set { - set(newValue, forKey: Key.mmAlertNewSensorDetected.rawValue) - } - } - - var mmAlertNoSensorDetected: Bool { - get { - optionalBool(forKey: Key.mmAlertNoSensorDetected.rawValue) ?? true - } - set { - set(newValue, forKey: Key.mmAlertNoSensorDetected.rawValue) - } - } - - var mmAlertWillSoonExpire: Bool { - get { - optionalBool(forKey: Key.mmAlertSensorSoonExpire.rawValue) ?? true - } - set { - set(newValue, forKey: Key.mmAlertSensorSoonExpire.rawValue) - } - } - - - - var mmShowTransmitterBattery: Bool { - get { - optionalBool(forKey: Key.mmShowTransmitterBattery.rawValue) ?? true - } - set { - set(newValue, forKey: Key.mmShowTransmitterBattery.rawValue) - } - } - - var allNotificationToggles: [Bool] { - [mmAlwaysDisplayGlucose, mmAlertLowBatteryWarning, - mmAlertInvalidSensorDetected, mmAlertNewSensorDetected, - mmAlertNoSensorDetected, mmAlertWillSoonExpire, - mmShowPhoneBattery, mmShowTransmitterBattery] - } - - var dangerModeActivated: Bool { - get { - optionalBool(forKey: Key.mmDangerMode.rawValue) ?? false - } - set { - set(newValue, forKey: Key.mmDangerMode.rawValue) - } - } - - // intentionally only supports mgdl and mmol - var mmGlucoseUnit: HKUnit? { - get { - if let textUnit = string(forKey: Key.mmGlucoseUnit.rawValue) { - if textUnit == "mmol" { - return HKUnit.millimolesPerLiter - } else if textUnit == "mgdl" { - return HKUnit.milligramsPerDeciliter - } - } - - return nil - } - set { - if newValue == HKUnit.milligramsPerDeciliter { - set("mgdl", forKey: Key.mmGlucoseUnit.rawValue) - } else if newValue == HKUnit.millimolesPerLiter { - set("mmol", forKey: Key.mmGlucoseUnit.rawValue) - } - } - } - - var enabledSchedules: [GlucoseSchedule]? { - glucoseSchedules?.schedules.compactMap({ schedule -> GlucoseSchedule? in - if schedule.enabled ?? false { - return schedule - } - return nil - }) - } - var snoozedUntil: Date? { - get { - object(forKey: Key.mmSnoozedUntil.rawValue) as? Date - } - set { - set(newValue, forKey: Key.mmSnoozedUntil.rawValue) - } - } - var glucoseSchedules: GlucoseScheduleList? { - get { - if let savedGlucoseSchedules = object(forKey: Key.glucoseSchedules.rawValue) as? Data { - let decoder = JSONDecoder() - if let loadedGlucoseSchedules = try? decoder.decode(GlucoseScheduleList.self, from: savedGlucoseSchedules) { - return loadedGlucoseSchedules - } - } - - return GlucoseScheduleList() - } - set { - let encoder = JSONEncoder() - if let val = newValue, let encoded = try? encoder.encode(val) { - set(encoded, forKey: Key.glucoseSchedules.rawValue) - } - } - } - - var mmCriticalAlarmsVolume: Double { - get { - double(forKey: Key.mmCriticalAlarmsVolume.rawValue) - } - set { - set(newValue, forKey: Key.mmCriticalAlarmsVolume.rawValue) - } - } -} diff --git a/Common/Settings/UserDefaults+Bluetooth.swift b/Common/Settings/UserDefaults+Bluetooth.swift index d17b682..6ef58e1 100644 --- a/Common/Settings/UserDefaults+Bluetooth.swift +++ b/Common/Settings/UserDefaults+Bluetooth.swift @@ -13,6 +13,23 @@ extension UserDefaults { private enum Key: String { case bluetoothDeviceUUIDString = "com.loopkit.librebluetoothDeviceUUIDString" case libre2UiD = "com.loopkit.libre2uid" + case dangerMode = "com.loopkit.libreDangerModeActivated" + } + + public func optionalBool(forKey defaultName: String) -> Bool? { + if let value = value(forKey: defaultName) { + return value as? Bool + } + return nil + } + + var dangerModeActivated: Bool { + get { + optionalBool(forKey: Key.dangerMode.rawValue) ?? false + } + set { + set(newValue, forKey: Key.dangerMode.rawValue) + } } public var preSelectedUid: Data? { diff --git a/Features.swift b/Features.swift index 0253e1d..ec5dde2 100644 --- a/Features.swift +++ b/Features.swift @@ -19,15 +19,7 @@ public final class Features { static public var allowsEditingFactoryCalibrationData = false static public var allowOneMinuteReadings = false - - // Uses Vibration through apples audio api for glucose alarms. This could be considered an api abuse from apple's standpoint; - // since apis invoked for this feature are meant for audio streaming apps. - // However, there are a couple of good reason for keeping this feature, but behind a featureflag rather than a gui toggle: - // * For heavy sleepers, the combination of an alarm at 100% volume + vibration makes it much more likely to wake up during nighttime. - // * For this feature to work as intended, Loops info.plist much be amended with "audio" permissions, - // see Loop->Signing & Capabilities->Background Modes->"Audio, AirPlay, Picture in picture" in xcode - static public var glucoseAlarmsAlsoCauseVibration = false - + static var phoneNFCAvailable: Bool { return NFCNDEFReaderSession.readingAvailable } diff --git a/LibreSensor/GlucoseAlgorithm/GlucoseSmoothing.swift b/LibreSensor/GlucoseAlgorithm/GlucoseSmoothing.swift deleted file mode 100644 index 0dbc9d3..0000000 --- a/LibreSensor/GlucoseAlgorithm/GlucoseSmoothing.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// GlucoseSmoothing.swift -// MiaomiaoClientUI -// -// Created by LoopKit Authors on 25/03/2019. -// Copyright © 2019 LoopKit Authors. All rights reserved. -// - -import Foundation - -// https://github.com/NightscoutFoundation/xDrip/pull/828/files - -// private func trendToLibreGlucose(_ measurements: [Measurement]) -> [LibreGlucose]?{ - -func CalculateSmothedData5Points(origtrends: [LibreGlucose]) -> [LibreGlucose] { - // In all places in the code, there should be exactly 16 points. - // Since that might change, and I'm doing an average of 5, then in the case of less then 5 points, - // I'll only copy the data as is (to make sure there are reasonable values when the function returns). - - var trends = origtrends - // this is an adoptation, doesn't follow the original directly - if trends.count < 5 { - for i in 0 ..< trends.count { - trends[i].glucoseDouble = trends[i].unsmoothedGlucose - } - - return trends - } - for i in 0 ..< trends.count - 4 { - trends[i].glucoseDouble = (trends[i].unsmoothedGlucose + trends[i + 1].unsmoothedGlucose + - trends[i + 2].unsmoothedGlucose + - trends[i + 3].unsmoothedGlucose + - trends[i + 4].unsmoothedGlucose) / 5 - } - trends[trends.count - 4].glucoseDouble = (trends[trends.count - 4].unsmoothedGlucose + - trends[trends.count - 3].unsmoothedGlucose + - trends[trends.count - 2].unsmoothedGlucose + - trends[trends.count - 1].unsmoothedGlucose) / 4 - - trends[trends.count - 3].glucoseDouble = (trends[trends.count - 3].unsmoothedGlucose + trends[trends.count - 2].unsmoothedGlucose + trends[trends.count - 1].unsmoothedGlucose ) / 3 - - trends[trends.count - 2].glucoseDouble = (trends[trends.count - 2].unsmoothedGlucose + trends[trends.count - 1].unsmoothedGlucose ) / 2 - - trends[trends.count - 1].glucoseDouble = trends[trends.count - 2].glucoseDouble - - return trends -} diff --git a/LibreTransmitter.xcodeproj/project.pbxproj b/LibreTransmitter.xcodeproj/project.pbxproj index 2e9477d..1a36533 100644 --- a/LibreTransmitter.xcodeproj/project.pbxproj +++ b/LibreTransmitter.xcodeproj/project.pbxproj @@ -7,8 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 270C1429266047490063405B /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270C1428266047490063405B /* NotificationSettingsView.swift */; }; - 271A39F52975844B0005FEDA /* NotificationHelperOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271A39F42975844B0005FEDA /* NotificationHelperOverride.swift */; }; + D42EC5E068404A889272225A /* LibreSensorLifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AFC6FD1B7BC4094B6402F98 /* LibreSensorLifecycle.swift */; }; + 06F862064B214505ABA15302 /* LibreAlertCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB7CEA2FC56B4E108F0B9FAC /* LibreAlertCondition.swift */; }; + DC0BD5CEC12B4881B6EA4101 /* LibreTransmitterManagerV3+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7021854176DD41F1AA5B4E1F /* LibreTransmitterManagerV3+Alerts.swift */; }; 2735A6E029637DEA00D4E868 /* LibreTransmitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 432B0E881CDFC3C50045347B /* LibreTransmitter.framework */; }; 2735A6E129637DEA00D4E868 /* LibreTransmitter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 432B0E881CDFC3C50045347B /* LibreTransmitter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 2735A6E429637DEA00D4E868 /* LibreTransmitterUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A8EC82210E664300A81379 /* LibreTransmitterUI.framework */; }; @@ -21,17 +22,14 @@ 2746C73F26DCF83700E31BD9 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C73D26DCF83400E31BD9 /* Features.swift */; }; 2746C74226DD0F8800E31BD9 /* Libre2DirectSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C74126DD0F8800E31BD9 /* Libre2DirectSetup.swift */; }; 274E71D3297ED77300FCFECD /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274E71D2297ED77300FCFECD /* AuthView.swift */; }; - 274E71D52986D4A600FCFECD /* CriticalAlarmsVolumeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274E71D42986D4A600FCFECD /* CriticalAlarmsVolumeView.swift */; }; 275786AB26753CC400845D0E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275786AA26753CC400845D0E /* SettingsView.swift */; }; 275EC993265AEE970043210E /* NumericTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275EC992265AEE970043210E /* NumericTextField.swift */; }; 275EC998265AF64E0043210E /* StatusMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275EC997265AF64E0043210E /* StatusMessage.swift */; }; 275EC9AB265EDC280043210E /* GlucoseSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275EC9AA265EDC280043210E /* GlucoseSettingsView.swift */; }; 2764E86126FC6DC10016A585 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C73D26DCF83400E31BD9 /* Features.swift */; }; - 276EF5E7264B1FCE00571021 /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276EF5E6264B1FCE00571021 /* AlarmSettingsView.swift */; }; 276EF5F22652F84F00571021 /* HashableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276EF5F12652F84F00571021 /* HashableClass.swift */; }; 277773A72639EF2B00431547 /* BlueButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277773A62639EF2B00431547 /* BlueButtonStyle.swift */; }; 277773AC2639EFB800431547 /* ErrorTextFieldStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277773AB2639EFB800431547 /* ErrorTextFieldStyle.swift */; }; - 277773B62639F51300431547 /* CustomDataPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277773B52639F51300431547 /* CustomDataPickerView.swift */; }; 27850CE725672C0C0020D109 /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CD925672C0C0020D109 /* DataExtensions.swift */; }; 27850CE925672C0C0020D109 /* TimeIntervalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CDA25672C0C0020D109 /* TimeIntervalExtensions.swift */; }; 27850CEA25672C0C0020D109 /* TimeIntervalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CDA25672C0C0020D109 /* TimeIntervalExtensions.swift */; }; @@ -45,26 +43,20 @@ 27850CF225672C0C0020D109 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CDE25672C0C0020D109 /* LocalizedString.swift */; }; 27850CF325672C0C0020D109 /* UserDefaults+GlucoseSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE025672C0C0020D109 /* UserDefaults+GlucoseSettings.swift */; }; 27850CF425672C0C0020D109 /* UserDefaults+GlucoseSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE025672C0C0020D109 /* UserDefaults+GlucoseSettings.swift */; }; - 27850CF525672C0C0020D109 /* UserDefaults+Alarmsettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE125672C0C0020D109 /* UserDefaults+Alarmsettings.swift */; }; - 27850CF625672C0C0020D109 /* UserDefaults+Alarmsettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE125672C0C0020D109 /* UserDefaults+Alarmsettings.swift */; }; 27850CF725672C0C0020D109 /* UserDefaults+Bluetooth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE225672C0C0020D109 /* UserDefaults+Bluetooth.swift */; }; 27850CF825672C0C0020D109 /* UserDefaults+Bluetooth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE225672C0C0020D109 /* UserDefaults+Bluetooth.swift */; }; 27850CF925672C0C0020D109 /* UIApplication+metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE325672C0C0020D109 /* UIApplication+metadata.swift */; }; 27850CFA25672C0C0020D109 /* UIApplication+metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE325672C0C0020D109 /* UIApplication+metadata.swift */; }; - 27850CFB25672C0C0020D109 /* GlucoseSchedules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE425672C0C0020D109 /* GlucoseSchedules.swift */; }; - 27850CFC25672C0C0020D109 /* GlucoseSchedules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE425672C0C0020D109 /* GlucoseSchedules.swift */; }; 27850CFD25672C0C0020D109 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE525672C0C0020D109 /* HKUnit.swift */; }; 27850CFE25672C0C0020D109 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850CE525672C0C0020D109 /* HKUnit.swift */; }; 27850D0E25672CA60020D109 /* ConcreteGlucoseDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D0B25672CA60020D109 /* ConcreteGlucoseDisplayable.swift */; }; 27850D1025672CA60020D109 /* LibreGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D0C25672CA60020D109 /* LibreGlucose.swift */; }; - 27850D1225672CA60020D109 /* NotificationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D0D25672CA60020D109 /* NotificationHelper.swift */; }; 27850D4E25672DFB0020D109 /* LibreTransmitterSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D2425672DFB0020D109 /* LibreTransmitterSetupViewController.swift */; }; 27850D6325672DFB0020D109 /* miaomiao-small.png in Resources */ = {isa = PBXBuildFile; fileRef = 27850D4025672DFB0020D109 /* miaomiao-small.png */; }; 27850D6425672DFB0020D109 /* libresensor.png in Resources */ = {isa = PBXBuildFile; fileRef = 27850D4125672DFB0020D109 /* libresensor.png */; }; 27850D6525672DFB0020D109 /* bubble.png in Resources */ = {isa = PBXBuildFile; fileRef = 27850D4225672DFB0020D109 /* bubble.png */; }; 27850D6925672DFB0020D109 /* icons8-up-50.png in Resources */ = {isa = PBXBuildFile; fileRef = 27850D4625672DFB0020D109 /* icons8-up-50.png */; }; 27850D6A25672DFB0020D109 /* icons8-down-arrow-50.png in Resources */ = {isa = PBXBuildFile; fileRef = 27850D4725672DFB0020D109 /* icons8-down-arrow-50.png */; }; - 27850D6C25672DFB0020D109 /* SnoozeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D4A25672DFB0020D109 /* SnoozeView.swift */; }; 27850D6D25672DFB0020D109 /* BluetoothSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D4B25672DFB0020D109 /* BluetoothSelection.swift */; }; 27850D9125672F020020D109 /* CBPeripheralExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BC7256725E70020D109 /* CBPeripheralExtensions.swift */; }; 27850D9225672F020020D109 /* LibreTransmitterMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BC6256725E70020D109 /* LibreTransmitterMetadata.swift */; }; @@ -75,7 +67,6 @@ 27850DA625672F1E0020D109 /* MiaomiaoTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BCA256725E70020D109 /* MiaomiaoTransmitter.swift */; }; 27850DB825672F4B0020D109 /* Calibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BA1256724EF0020D109 /* Calibration.swift */; }; 27850DBE25672F530020D109 /* LibreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BA0256724EF0020D109 /* LibreError.swift */; }; - 27850DC425672F640020D109 /* GlucoseSmoothing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BA5256724F00020D109 /* GlucoseSmoothing.swift */; }; 27850DC525672F640020D109 /* GlucoseFromRaw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BA4256724EF0020D109 /* GlucoseFromRaw.swift */; }; 27850DCB25672F770020D109 /* SensorSerialNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BAC256724F10020D109 /* SensorSerialNumber.swift */; }; 27850DCC25672F770020D109 /* Measurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BA8256724F00020D109 /* Measurement.swift */; }; @@ -94,7 +85,6 @@ 27DD8F26268FA75A00649010 /* SensorInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DD8F25268FA75A00649010 /* SensorInfo.swift */; }; 27DD8F28268FA79800649010 /* GlucoseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DD8F27268FA79800649010 /* GlucoseInfo.swift */; }; 27DD8F2A269106A100649010 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DD8F29269106A100649010 /* ViewExtensions.swift */; }; - 27ED67B82698EF38003E5DAB /* AlarmStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED67B72698EF38003E5DAB /* AlarmStatus.swift */; }; 27ED67BA26990D6B003E5DAB /* GenericObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED67B926990D6B003E5DAB /* GenericObservableObject.swift */; }; 27F93B2A2816B7FF00EE39A7 /* LibreTransmitterManager+Libre2EU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F93B292816B7FF00EE39A7 /* LibreTransmitterManager+Libre2EU.swift */; }; 27F93B2C2816B93900EE39A7 /* LibreTransmitterManager+Transmitters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F93B2B2816B93900EE39A7 /* LibreTransmitterManager+Transmitters.swift */; }; @@ -202,9 +192,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2AFC6FD1B7BC4094B6402F98 /* LibreSensorLifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreSensorLifecycle.swift; sourceTree = ""; }; + CB7CEA2FC56B4E108F0B9FAC /* LibreAlertCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreAlertCondition.swift; sourceTree = ""; }; + 7021854176DD41F1AA5B4E1F /* LibreTransmitterManagerV3+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibreTransmitterManagerV3+Alerts.swift"; sourceTree = ""; }; 2709A6F229E5E0D3004355A3 /* FakeSensorPairingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeSensorPairingData.swift; sourceTree = ""; }; - 270C1428266047490063405B /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = ""; }; - 271A39F42975844B0005FEDA /* NotificationHelperOverride.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHelperOverride.swift; sourceTree = ""; }; 27394068296C2A48001DD18D /* libresensor200.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = libresensor200.png; sourceTree = ""; }; 274557C72979EA38003B027B /* LoggerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerExtension.swift; sourceTree = ""; }; 2746C73B26D91FC900E31BD9 /* Libre2DirectTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Libre2DirectTransmitter.swift; sourceTree = ""; }; @@ -213,21 +204,17 @@ 2746C74426DF636900E31BD9 /* SensorPairingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorPairingService.swift; sourceTree = ""; }; 2746C74626DF63C800E31BD9 /* SensorPairing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorPairing.swift; sourceTree = ""; }; 274E71D2297ED77300FCFECD /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = ""; }; - 274E71D42986D4A600FCFECD /* CriticalAlarmsVolumeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalAlarmsVolumeView.swift; sourceTree = ""; }; 275786AA26753CC400845D0E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 275EC992265AEE970043210E /* NumericTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericTextField.swift; sourceTree = ""; }; 275EC997265AF64E0043210E /* StatusMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMessage.swift; sourceTree = ""; }; 275EC9AA265EDC280043210E /* GlucoseSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSettingsView.swift; sourceTree = ""; }; - 276EF5E6264B1FCE00571021 /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = ""; }; 276EF5F12652F84F00571021 /* HashableClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashableClass.swift; sourceTree = ""; }; 277773A62639EF2B00431547 /* BlueButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueButtonStyle.swift; sourceTree = ""; }; 277773AB2639EFB800431547 /* ErrorTextFieldStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTextFieldStyle.swift; sourceTree = ""; }; - 277773B52639F51300431547 /* CustomDataPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDataPickerView.swift; sourceTree = ""; }; 27850BA0256724EF0020D109 /* LibreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreError.swift; sourceTree = ""; }; 27850BA1256724EF0020D109 /* Calibration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Calibration.swift; sourceTree = ""; }; 27850BA2256724EF0020D109 /* LimitedQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LimitedQueue.swift; sourceTree = ""; }; 27850BA4256724EF0020D109 /* GlucoseFromRaw.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseFromRaw.swift; sourceTree = ""; }; - 27850BA5256724F00020D109 /* GlucoseSmoothing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSmoothing.swift; sourceTree = ""; }; 27850BA6256724F00020D109 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 27850BA8256724F00020D109 /* Measurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurement.swift; sourceTree = ""; }; 27850BA9256724F00020D109 /* PreLibre2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreLibre2.swift; sourceTree = ""; }; @@ -251,21 +238,17 @@ 27850CDD25672C0C0020D109 /* CollectionExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = ""; }; 27850CDE25672C0C0020D109 /* LocalizedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizedString.swift; sourceTree = ""; }; 27850CE025672C0C0020D109 /* UserDefaults+GlucoseSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+GlucoseSettings.swift"; sourceTree = ""; }; - 27850CE125672C0C0020D109 /* UserDefaults+Alarmsettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Alarmsettings.swift"; sourceTree = ""; }; 27850CE225672C0C0020D109 /* UserDefaults+Bluetooth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Bluetooth.swift"; sourceTree = ""; }; 27850CE325672C0C0020D109 /* UIApplication+metadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+metadata.swift"; sourceTree = ""; }; - 27850CE425672C0C0020D109 /* GlucoseSchedules.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseSchedules.swift; sourceTree = ""; }; 27850CE525672C0C0020D109 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; 27850D0B25672CA60020D109 /* ConcreteGlucoseDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConcreteGlucoseDisplayable.swift; sourceTree = ""; }; 27850D0C25672CA60020D109 /* LibreGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreGlucose.swift; sourceTree = ""; }; - 27850D0D25672CA60020D109 /* NotificationHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationHelper.swift; sourceTree = ""; }; 27850D2425672DFB0020D109 /* LibreTransmitterSetupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibreTransmitterSetupViewController.swift; sourceTree = ""; }; 27850D4025672DFB0020D109 /* miaomiao-small.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "miaomiao-small.png"; sourceTree = ""; }; 27850D4125672DFB0020D109 /* libresensor.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = libresensor.png; sourceTree = ""; }; 27850D4225672DFB0020D109 /* bubble.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bubble.png; sourceTree = ""; }; 27850D4625672DFB0020D109 /* icons8-up-50.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icons8-up-50.png"; sourceTree = ""; }; 27850D4725672DFB0020D109 /* icons8-down-arrow-50.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icons8-down-arrow-50.png"; sourceTree = ""; }; - 27850D4A25672DFB0020D109 /* SnoozeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnoozeView.swift; sourceTree = ""; }; 27850D4B25672DFB0020D109 /* BluetoothSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothSelection.swift; sourceTree = ""; }; 2793EBE5260BDE43001A35A3 /* CalibrationEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationEditView.swift; sourceTree = ""; }; 27A5912626E0E32C00A7EE36 /* ModeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeSelectionView.swift; sourceTree = ""; }; @@ -274,7 +257,6 @@ 27DD8F25268FA75A00649010 /* SensorInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorInfo.swift; sourceTree = ""; }; 27DD8F27268FA79800649010 /* GlucoseInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseInfo.swift; sourceTree = ""; }; 27DD8F29269106A100649010 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; }; - 27ED67B72698EF38003E5DAB /* AlarmStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmStatus.swift; sourceTree = ""; }; 27ED67B926990D6B003E5DAB /* GenericObservableObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericObservableObject.swift; sourceTree = ""; }; 27F1592E26CAF6BE00EBA666 /* GenericThrottler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericThrottler.swift; sourceTree = ""; }; 27F93B292816B7FF00EE39A7 /* LibreTransmitterManager+Libre2EU.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibreTransmitterManager+Libre2EU.swift"; sourceTree = ""; }; @@ -397,16 +379,6 @@ path = Styles; sourceTree = ""; }; - 277773B42639F4E300431547 /* AlarmSettings */ = { - isa = PBXGroup; - children = ( - 277773B52639F51300431547 /* CustomDataPickerView.swift */, - 276EF5E6264B1FCE00571021 /* AlarmSettingsView.swift */, - 274E71D42986D4A600FCFECD /* CriticalAlarmsVolumeView.swift */, - ); - path = AlarmSettings; - sourceTree = ""; - }; 27850B9F256724EE0020D109 /* LibreSensor */ = { isa = PBXGroup; children = ( @@ -423,7 +395,6 @@ 27850BA3256724EF0020D109 /* GlucoseAlgorithm */ = { isa = PBXGroup; children = ( - 27850BA5256724F00020D109 /* GlucoseSmoothing.swift */, 27850BA4256724EF0020D109 /* GlucoseFromRaw.swift */, 27850BA6256724F00020D109 /* .gitignore */, ); @@ -495,10 +466,8 @@ isa = PBXGroup; children = ( 27850CE025672C0C0020D109 /* UserDefaults+GlucoseSettings.swift */, - 27850CE125672C0C0020D109 /* UserDefaults+Alarmsettings.swift */, 27850CE225672C0C0020D109 /* UserDefaults+Bluetooth.swift */, 27850CE325672C0C0020D109 /* UIApplication+metadata.swift */, - 27850CE425672C0C0020D109 /* GlucoseSchedules.swift */, ); path = Settings; sourceTree = ""; @@ -540,11 +509,8 @@ isa = PBXGroup; children = ( 275786AA26753CC400845D0E /* SettingsView.swift */, - 27850D4A25672DFB0020D109 /* SnoozeView.swift */, 2793EBE5260BDE43001A35A3 /* CalibrationEditView.swift */, 275EC9AA265EDC280043210E /* GlucoseSettingsView.swift */, - 270C1428266047490063405B /* NotificationSettingsView.swift */, - 277773B42639F4E300431547 /* AlarmSettings */, ); path = Settings; sourceTree = ""; @@ -555,12 +521,21 @@ 27DD8F22268FA0F500649010 /* TransmitterInfo.swift */, 27DD8F25268FA75A00649010 /* SensorInfo.swift */, 27DD8F27268FA79800649010 /* GlucoseInfo.swift */, - 27ED67B72698EF38003E5DAB /* AlarmStatus.swift */, C1AF65972A49C902008D0690 /* SelectionState.swift */, ); path = Observables; sourceTree = ""; }; + A8D5D1B79EF34BA3AEC8C642 /* Alerting */ = { + isa = PBXGroup; + children = ( + 2AFC6FD1B7BC4094B6402F98 /* LibreSensorLifecycle.swift */, + CB7CEA2FC56B4E108F0B9FAC /* LibreAlertCondition.swift */, + 7021854176DD41F1AA5B4E1F /* LibreTransmitterManagerV3+Alerts.swift */, + ); + path = Alerting; + sourceTree = ""; + }; 432B0E7E1CDFC3C50045347B = { isa = PBXGroup; children = ( @@ -595,9 +570,8 @@ children = ( C1C318D42A47637100C6F29F /* Mocks */, 27ED67B62698EEEF003E5DAB /* Observables */, + A8D5D1B79EF34BA3AEC8C642 /* Alerting */, 27850D0B25672CA60020D109 /* ConcreteGlucoseDisplayable.swift */, - 27850D0D25672CA60020D109 /* NotificationHelper.swift */, - 271A39F42975844B0005FEDA /* NotificationHelperOverride.swift */, 27850D0C25672CA60020D109 /* LibreGlucose.swift */, B66D1F722E6A813000471149 /* Localizable.xcstrings */, 43A8EC9C210E68CE00A81379 /* LibreTransmitterManagerV3.swift */, @@ -941,6 +915,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D42EC5E068404A889272225A /* LibreSensorLifecycle.swift in Sources */, + 06F862064B214505ABA15302 /* LibreAlertCondition.swift in Sources */, + DC0BD5CEC12B4881B6EA4101 /* LibreTransmitterManagerV3+Alerts.swift in Sources */, 27850CF125672C0C0020D109 /* LocalizedString.swift in Sources */, 27850D1025672CA60020D109 /* LibreGlucose.swift in Sources */, 27850D9E25672F170020D109 /* UUIDContainer.swift in Sources */, @@ -949,7 +926,6 @@ 2746C73C26D91FC900E31BD9 /* Libre2DirectTransmitter.swift in Sources */, 27F93B2C2816B93900EE39A7 /* LibreTransmitterManager+Transmitters.swift in Sources */, C1AF65982A49C902008D0690 /* SelectionState.swift in Sources */, - 27850CFB25672C0C0020D109 /* GlucoseSchedules.swift in Sources */, C1C318CD2A47468D00C6F29F /* BluetoothSearch.swift in Sources */, 27850D9125672F020020D109 /* CBPeripheralExtensions.swift in Sources */, 27850DA625672F1E0020D109 /* MiaomiaoTransmitter.swift in Sources */, @@ -978,23 +954,18 @@ 27850DA525672F1C0020D109 /* LibreTransmitterProxyManager.swift in Sources */, 27850CF725672C0C0020D109 /* UserDefaults+Bluetooth.swift in Sources */, C1AF1D122A4BC18400F46A26 /* MockSensorData.swift in Sources */, - 271A39F52975844B0005FEDA /* NotificationHelperOverride.swift in Sources */, 27850CE725672C0C0020D109 /* DataExtensions.swift in Sources */, - 27850D1225672CA60020D109 /* NotificationHelper.swift in Sources */, 27850CF325672C0C0020D109 /* UserDefaults+GlucoseSettings.swift in Sources */, 27850CE925672C0C0020D109 /* TimeIntervalExtensions.swift in Sources */, C1D819B32AC9B60100C9416E /* LimitedQueue.swift in Sources */, 27DD8F28268FA79800649010 /* GlucoseInfo.swift in Sources */, 27850DD025672F780020D109 /* PreLibre2.swift in Sources */, 27DD8F23268FA0F500649010 /* TransmitterInfo.swift in Sources */, - 27850CF525672C0C0020D109 /* UserDefaults+Alarmsettings.swift in Sources */, 27850D9225672F020020D109 /* LibreTransmitterMetadata.swift in Sources */, 27DD8F26268FA75A00649010 /* SensorInfo.swift in Sources */, - 27850DC425672F640020D109 /* GlucoseSmoothing.swift in Sources */, 27F93B2A2816B7FF00EE39A7 /* LibreTransmitterManager+Libre2EU.swift in Sources */, 27850DCF25672F780020D109 /* CRC.swift in Sources */, 2764E86126FC6DC10016A585 /* Features.swift in Sources */, - 27ED67B82698EF38003E5DAB /* AlarmStatus.swift in Sources */, 27850CFD25672C0C0020D109 /* HKUnit.swift in Sources */, 27850CF925672C0C0020D109 /* UIApplication+metadata.swift in Sources */, ); @@ -1004,7 +975,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 274E71D52986D4A600FCFECD /* CriticalAlarmsVolumeView.swift in Sources */, 2746C74226DD0F8800E31BD9 /* Libre2DirectSetup.swift in Sources */, 27ED67BA26990D6B003E5DAB /* GenericObservableObject.swift in Sources */, 27850CFE25672C0C0020D109 /* HKUnit.swift in Sources */, @@ -1016,26 +986,20 @@ 274E71D3297ED77300FCFECD /* AuthView.swift in Sources */, 27850CEA25672C0C0020D109 /* TimeIntervalExtensions.swift in Sources */, 275EC998265AF64E0043210E /* StatusMessage.swift in Sources */, - 27850D6C25672DFB0020D109 /* SnoozeView.swift in Sources */, 274557C92979EA38003B027B /* LoggerExtension.swift in Sources */, - 276EF5E7264B1FCE00571021 /* AlarmSettingsView.swift in Sources */, 27850D6D25672DFB0020D109 /* BluetoothSelection.swift in Sources */, - 270C1429266047490063405B /* NotificationSettingsView.swift in Sources */, 2793EBE6260BDE43001A35A3 /* CalibrationEditView.swift in Sources */, 27850CF225672C0C0020D109 /* LocalizedString.swift in Sources */, - 27850CF625672C0C0020D109 /* UserDefaults+Alarmsettings.swift in Sources */, 27850CEE25672C0C0020D109 /* DateExtensions.swift in Sources */, 27850CF425672C0C0020D109 /* UserDefaults+GlucoseSettings.swift in Sources */, 27A5912726E0E32C00A7EE36 /* ModeSelectionView.swift in Sources */, 2746C73F26DCF83700E31BD9 /* Features.swift in Sources */, 27850CEC25672C0C0020D109 /* DoubleExtensions.swift in Sources */, 277773AC2639EFB800431547 /* ErrorTextFieldStyle.swift in Sources */, - 277773B62639F51300431547 /* CustomDataPickerView.swift in Sources */, 43A8EC91210E676500A81379 /* LibreTransmitterManager+UI.swift in Sources */, 27850CFA25672C0C0020D109 /* UIApplication+metadata.swift in Sources */, 27850CF025672C0C0020D109 /* CollectionExtensions.swift in Sources */, 277773A72639EF2B00431547 /* BlueButtonStyle.swift in Sources */, - 27850CFC25672C0C0020D109 /* GlucoseSchedules.swift in Sources */, 27DD8F2A269106A100649010 /* ViewExtensions.swift in Sources */, 275EC993265AEE970043210E /* NumericTextField.swift in Sources */, ); diff --git a/LibreTransmitter/.gitignore b/LibreTransmitter/.gitignore deleted file mode 100644 index b626749..0000000 --- a/LibreTransmitter/.gitignore +++ /dev/null @@ -1 +0,0 @@ -NotificationHelperOverride.swift diff --git a/LibreTransmitter/Alerting/LibreAlertCondition.swift b/LibreTransmitter/Alerting/LibreAlertCondition.swift new file mode 100644 index 0000000..7fa645d --- /dev/null +++ b/LibreTransmitter/Alerting/LibreAlertCondition.swift @@ -0,0 +1,89 @@ +// +// LibreAlertCondition.swift +// LibreTransmitter +// +// Created by LoopKit Authors. +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit + +/// Sensor-lifecycle conditions that are surfaced to the user via LoopKit's native +/// Alert framework (`issueAlert`/`retractAlert`). Glucose-threshold alerting is owned +/// by the app (Trio), not by this plugin. +public enum LibreAlertCondition: String, CaseIterable, Sendable { + case signalLost + case expired + case failed + case unactivated + + public static func currentlyFiring(for lifecycle: LibreSensorLifecycle) -> Set { + switch lifecycle { + case .signalLost: + return [.signalLost] + case .expired: + return [.expired] + case .failed: + return [.failed] + case .unactivated: + return [.unactivated] + case .active, .noSensor, .warmup: + return [] + } + } + + public func identifier(managerIdentifier: String) -> Alert.Identifier { + Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "libre.\(rawValue)") + } + + private var title: String { + switch self { + case .signalLost: + return LocalizedString("Signal Loss", comment: "Title for signal loss sensor alert") + case .expired: + return LocalizedString("Sensor Expired", comment: "Title for sensor expired alert") + case .failed: + return LocalizedString("Sensor Malfunction", comment: "Title for sensor malfunction alert") + case .unactivated: + return LocalizedString("Sensor Not Detected", comment: "Title for sensor not detected alert") + } + } + + private var body: String { + switch self { + case .signalLost: + return LocalizedString("Signal lost. Check that your sensor is nearby and Bluetooth is on.", comment: "Body for signal loss sensor alert") + case .expired: + return LocalizedString("Sensor expired. Replace your sensor now.", comment: "Body for sensor expired alert") + case .failed: + return LocalizedString("Sensor malfunction. Replace your sensor now.", comment: "Body for sensor malfunction alert") + case .unactivated: + return LocalizedString("Sensor not detected or reporting as invalid. If this continues, remove and re-pair it.", comment: "Body for sensor not detected alert") + } + } + + private var interruptionLevel: Alert.InterruptionLevel { + switch self { + case .signalLost, .expired: + return .timeSensitive + case .failed, .unactivated: + return .critical + } + } + + public func alert(managerIdentifier: String) -> Alert { + let content = Alert.Content( + title: title, + body: body, + acknowledgeActionButtonLabel: LocalizedString("OK", comment: "Alert acknowledge button") + ) + return Alert( + identifier: identifier(managerIdentifier: managerIdentifier), + foregroundContent: content, + backgroundContent: content, + trigger: .immediate, + interruptionLevel: interruptionLevel + ) + } +} diff --git a/LibreTransmitter/Alerting/LibreSensorLifecycle.swift b/LibreTransmitter/Alerting/LibreSensorLifecycle.swift new file mode 100644 index 0000000..fd24431 --- /dev/null +++ b/LibreTransmitter/Alerting/LibreSensorLifecycle.swift @@ -0,0 +1,78 @@ +// +// LibreSensorLifecycle.swift +// LibreTransmitter +// +// Created by LoopKit Authors. +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation + +/// A unified view of sensor lifecycle state across both LibreTransmitter data paths +/// (classic BLE transmitters like MiaoMiao/Bubble, and direct Libre2 BLE). +public enum LibreSensorLifecycle: Equatable { + case noSensor + case warmup(progress: Double, remaining: TimeInterval) + case active(remaining: TimeInterval, total: TimeInterval) + case expired + case signalLost(since: Date) + case failed + case unactivated + + /// A normalized, cross-data-path representation of a hard sensor fault. + public enum FaultKind: Equatable { + case sensorFailure + case encryptedOrUnsupported + case noSensorFound + } + + static let signalLostThreshold: TimeInterval = .minutes(6) + + public static func compute( + sensorPaired: Bool, + activatedAt: Date?, + expiresAt: Date?, + latestReadingAt: Date?, + sensorMaxMinutesWearTime: Int, + sensorState: SensorState?, + lastFault: FaultKind?, + now: Date = Date() + ) -> LibreSensorLifecycle { + guard sensorPaired else { + return .noSensor + } + + switch lastFault { + case .sensorFailure: + return .failed + case .encryptedOrUnsupported, .noSensorFound: + return .unactivated + case nil: + break + } + + guard let activatedAt, let expiresAt, sensorMaxMinutesWearTime > 0 else { + return .noSensor + } + + let age = now.timeIntervalSince(activatedAt) + let wear = TimeInterval(minutes: Double(sensorMaxMinutesWearTime)) + + if age >= wear || now >= expiresAt { + return .expired + } + + if sensorState == .starting || sensorState == .notYetStarted { + return .warmup(progress: age / wear, remaining: wear - age) + } + + // If we've never received a successful reading yet, treat this as an + // initial grace period rather than signal loss, to avoid false positives + // immediately after pairing/activation. + if let latestReadingAt, now.timeIntervalSince(latestReadingAt) > signalLostThreshold { + return .signalLost(since: latestReadingAt) + } + + return .active(remaining: wear - age, total: wear) + } +} diff --git a/LibreTransmitter/Alerting/LibreTransmitterManagerV3+Alerts.swift b/LibreTransmitter/Alerting/LibreTransmitterManagerV3+Alerts.swift new file mode 100644 index 0000000..44ad203 --- /dev/null +++ b/LibreTransmitter/Alerting/LibreTransmitterManagerV3+Alerts.swift @@ -0,0 +1,39 @@ +// +// LibreTransmitterManagerV3+Alerts.swift +// LibreTransmitter +// +// Created by LoopKit Authors. +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit + +extension LibreTransmitterManagerV3 { + /// Diffs the currently-firing lifecycle alert conditions against the previous + /// evaluation and issues/retracts LoopKit Alerts for the delta. +func evaluateAlerts() { + // Serialize evaluation + state mutation to avoid races between delegate callbacks and polling. + delegateQueue.async { [weak self] in + guard let self else { return } + + let shouldFire = LibreAlertCondition.currentlyFiring(for: self.sensorLifecycle) + guard shouldFire != self.firingAlertConditions else { + return + } + + let delegate = self.cgmManagerDelegate + let managerIdentifier = Self.pluginIdentifier + let newlyFiring = shouldFire.subtracting(self.firingAlertConditions) + let noLongerFiring = self.firingAlertConditions.subtracting(shouldFire) + + for condition in newlyFiring { + delegate?.issueAlert(condition.alert(managerIdentifier: managerIdentifier)) + } + for condition in noLongerFiring { + delegate?.retractAlert(identifier: condition.identifier(managerIdentifier: managerIdentifier)) + } + + self.firingAlertConditions = shouldFire + } +} diff --git a/LibreTransmitter/LibreGlucose.swift b/LibreTransmitter/LibreGlucose.swift index 43ec73c..65c0279 100644 --- a/LibreTransmitter/LibreGlucose.swift +++ b/LibreTransmitter/LibreGlucose.swift @@ -128,15 +128,14 @@ extension LibreGlucose { static func fromTrendMeasurements(_ measurements: [Measurement], nativeCalibrationData: SensorData.CalibrationInfo) -> [LibreGlucose] { var arr = [LibreGlucose]() - var shouldSmoothGlucose = true for trend in measurements { // trend arrows on each libreglucose value is not needed // instead we calculate it once when latestbackfill is set, which in turn sets // the sensordisplayable property + let calibrated = trend.calibratedGlucose(calibrationInfo: nativeCalibrationData) let glucose = LibreGlucose( - // unsmoothedGlucose: trend.temperatureAlgorithmGlucose, - unsmoothedGlucose: trend.calibratedGlucose(calibrationInfo: nativeCalibrationData), - glucoseDouble: 0.0, + unsmoothedGlucose: calibrated, + glucoseDouble: calibrated, error: trend.error, timestamp: trend.date) // if sensor is ripped off body while transmitter is attached, values below 1 might be created @@ -144,20 +143,6 @@ extension LibreGlucose { if glucose.unsmoothedGlucose > 0 && glucose.unsmoothedGlucose <= 500 { arr.append(glucose) } - - // Just for expliciticity, if one of the values are 0, - // then the rest of the values should not be smoothed - if glucose.unsmoothedGlucose <= 0 { - shouldSmoothGlucose = false - } - } - - if shouldSmoothGlucose { - arr = CalculateSmothedData5Points(origtrends: arr) - } else { - for i in 0 ..< arr.count { - arr[i].glucoseDouble = arr[i].unsmoothedGlucose - } } return arr diff --git a/LibreTransmitter/LibreTransmitterManager+Libre2EU.swift b/LibreTransmitter/LibreTransmitterManager+Libre2EU.swift index 257f315..e56c7e6 100644 --- a/LibreTransmitter/LibreTransmitterManager+Libre2EU.swift +++ b/LibreTransmitter/LibreTransmitterManager+Libre2EU.swift @@ -65,13 +65,6 @@ extension LibreTransmitterManagerV3 { return } - if sensor.maxAge > 0 { - let minutesLeft = Double(sensor.maxAge - bleData.age) - NotificationHelper.sendSensorExpireAlertIfNeeded(minutesLeft: minutesLeft) - - } - - verifySensorChange(for: sensor.uuid, activatedAt: Date() - TimeInterval(minutes: Double(bleData.age))) diff --git a/LibreTransmitter/LibreTransmitterManager+Transmitters.swift b/LibreTransmitter/LibreTransmitterManager+Transmitters.swift index 6c5d7f6..f2f33c0 100644 --- a/LibreTransmitter/LibreTransmitterManager+Transmitters.swift +++ b/LibreTransmitter/LibreTransmitterManager+Transmitters.swift @@ -13,7 +13,6 @@ import LoopKit extension LibreTransmitterManagerV3 { public func noLibreTransmitterSelected() { - NotificationHelper.sendNoTransmitterSelectedNotification() } public func libreTransmitterDidUpdate(with sensorData: SensorData, and Device: LibreTransmitterMetadata) { @@ -21,7 +20,6 @@ extension LibreTransmitterManagerV3 { self.logger.debug("got sensordata: \(String(describing: sensorData)), bytescount: \( sensorData.bytes.count), bytes: \(sensorData.bytes)") var sensorData = sensorData - NotificationHelper.sendLowBatteryNotificationIfNeeded(device: Device) self.setObservables(sensorData: nil, bleData: nil, metaData: Device) if !sensorData.isLikelyLibre1FRAM { @@ -33,7 +31,10 @@ extension LibreTransmitterManagerV3 { } } else { logger.debug("Sensor type was incorrect, and no decryption of sensor was possible") - self.cgmManagerDelegate?.cgmManager(self, hasNew: .error(LibreError.encryptedSensor)) + self.lastFault = .encryptedOrUnsupported + self.delegateQueue.async { + self.cgmManagerDelegate?.cgmManager(self, hasNew: .error(LibreError.encryptedSensor)) + } return } } @@ -44,8 +45,7 @@ extension LibreTransmitterManagerV3 { tryPersistSensorData(with: sensorData) - NotificationHelper.sendInvalidSensorNotificationIfNeeded(sensorData: sensorData) - NotificationHelper.sendInvalidChecksumIfDeveloper(sensorData) + self.lastKnownSensorState = sensorData.state guard sensorData.hasValidCRCs else { self.delegateQueue.async { @@ -56,20 +56,24 @@ extension LibreTransmitterManagerV3 { return } - NotificationHelper.sendSensorExpireAlertIfNeeded(sensorData: sensorData) - guard sensorData.state == .ready || sensorData.state == .starting else { logger.debug("got sensordata with valid crcs, but sensor is either expired or failed") + if sensorData.state == .failure || sensorData.state == .shutdown { + self.lastFault = .sensorFailure + } self.delegateQueue.async { self.cgmManagerDelegate?.cgmManager(self, hasNew: .error(LibreError.expiredSensor)) } return } + // sensor is reporting a healthy state again, clear any previously recorded fault + self.lastFault = nil + logger.debug("got sensordata with valid crcs, sensor was ready") // self.lastValidSensorData = sensorData - + verifySensorChange(for: sensorData.uuid, activatedAt: Date() - TimeInterval(minutes: Double(sensorData.minutesSinceStart))) @@ -203,14 +207,12 @@ extension LibreTransmitterManagerV3 { do { try KeychainManager.standard.setLibreNativeCalibrationData(calibrationparams) } catch { - NotificationHelper.sendCalibrationNotification(.invalidCalibrationData) callback(.invalidCalibrationData, nil) return } // here we assume success, data is not changed, // and we trust that the remote endpoint returns correct data for the sensor - NotificationHelper.sendCalibrationNotification(.success) callback(nil, self?.readingToGlucose(data, calibration: calibrationparams)) } } @@ -250,10 +252,9 @@ extension LibreTransmitterManagerV3 { case .newSensor: //we can't be sure of the activation datetime for the new sensor here logger.debug("New libresensor detected") - NotificationHelper.sendSensorChangeNotificationIfNeeded() case .noSensor: logger.debug("No libresensor detected") - NotificationHelper.sendSensorNotDetectedNotificationIfNeeded(noSensor: true) + self.lastFault = .noSensorFound default: // we don't care about the rest! break diff --git a/LibreTransmitter/LibreTransmitterManagerV3.swift b/LibreTransmitter/LibreTransmitterManagerV3.swift index 93973a3..3974424 100644 --- a/LibreTransmitter/LibreTransmitterManagerV3.swift +++ b/LibreTransmitter/LibreTransmitterManagerV3.swift @@ -35,7 +35,35 @@ open class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelegate { } public var cgmManagerStatus: CGMManagerStatus { - CGMManagerStatus(hasValidSensorSession: hasValidSensorSession, device: nil) + CGMManagerStatus(hasValidSensorSession: hasValidSensorSession, lastCommunicationDate: latestReadingTimestamp, device: nil) + } + + /// Tracks which `LibreAlertCondition`s were issued as of the last `evaluateAlerts()` call, + /// so it can diff against the currently-firing set and retract anything that's resolved. + var firingAlertConditions: Set = [] + + /// Timestamp of the most recent successful glucose reading, from either data path. + /// Used to detect signal loss (absence of readings) and to populate `CGMManagerStatus.lastCommunicationDate`. + var latestReadingTimestamp: Date? + + /// The most recently observed hard sensor fault, normalized across both data paths. + /// Cleared once the sensor recovers to a normal reporting state. + var lastFault: LibreSensorLifecycle.FaultKind? + + /// The most recently reported `SensorState` byte from a classic (FRAM-reading) transmitter. + /// Direct-BLE Libre2 has no equivalent signal and leaves this `nil`. + var lastKnownSensorState: SensorState? + + public var sensorLifecycle: LibreSensorLifecycle { + LibreSensorLifecycle.compute( + sensorPaired: hasValidSensorSession, + activatedAt: sensorInfoObservable.activatedAt, + expiresAt: sensorInfoObservable.expiresAt, + latestReadingAt: latestReadingTimestamp, + sensorMaxMinutesWearTime: sensorInfoObservable.sensorMaxMinutesWearTime, + sensorState: lastKnownSensorState, + lastFault: lastFault + ) } public var glucoseDisplay: GlucoseDisplayable? @@ -167,13 +195,17 @@ open class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelegate { public func fetchNewDataIfNeeded(_ completion: @escaping (CGMReadingResult) -> Void) { logger.debug("fetchNewDataIfNeeded called but we don't continue") + // Real data delivery happens via the delegate callbacks (libreTransmitterDidUpdate / + // libreSensorDidUpdate), not through this polling entry point. However, this is the only + // place we can detect "silence" (no callback at all), which is what signal-loss means - + // so use it as a periodic safety net for evaluating lifecycle alerts. + evaluateAlerts() + completion(.noData) } public var lastConnected: Date? - public internal(set) var alarmStatus = AlarmStatus() - internal var latestPrediction: LibreGlucose? public var latestBackfill: LibreGlucose? { @@ -186,26 +218,8 @@ open class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelegate { let oldValue = latestBackfill defer { - logger.debug("sending glucose notification") - NotificationHelper.sendGlucoseNotificationIfNeeded(glucose: newValue, - oldValue: oldValue, - trend: trend, - battery: proxy?.metadata?.batteryString ?? "n/a", - glucoseFormatter: alertsUnitPreference.formatter) - - // once we have a new glucose value, we can update the isalarming property - if let activeAlarms = UserDefaults.standard.glucoseSchedules?.getActiveAlarms(newValue.glucoseDouble) { - DispatchQueue.main.async { - self.alarmStatus.isAlarming = ([.high, .low].contains(activeAlarms)) - self.alarmStatus.glucoseScheduleAlarmResult = activeAlarms - } - } else { - DispatchQueue.main.async { - self.alarmStatus.isAlarming = false - self.alarmStatus.glucoseScheduleAlarmResult = .none - } - } - + latestReadingTimestamp = newValue.startDate + evaluateAlerts() } logger.debug("latestBackfill set, newvalue is \(newValue.glucose)") @@ -254,7 +268,6 @@ open class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelegate { lastConnected = nil logger.debug("LibreTransmitterManager will be created now") - NotificationHelper.requestNotificationPermissionsIfNeeded() if isDeviceSelected { establishProxy() diff --git a/LibreTransmitter/NotificationHelper.swift b/LibreTransmitter/NotificationHelper.swift deleted file mode 100644 index 12d9f90..0000000 --- a/LibreTransmitter/NotificationHelper.swift +++ /dev/null @@ -1,491 +0,0 @@ -// -// NotificationHelper.swift -// MiaomiaoClient -// -// Created by LoopKit Authors on 30/05/2019. -// Copyright © 2019 LoopKit Authors. All rights reserved. -// - -import AudioToolbox -import Foundation -import HealthKit -import LoopKit -import UserNotifications -import os.log - -private var logger = Logger(forType: "NotificationHelper") -// MARK: - Notification Utilities -public enum NotificationHelper { - - private enum Identifiers: String { - case glucocoseNotifications = "com.loopkit.libremiaomiao.glucose-notification" - case noSensorDetected = "com.loopkit.libremiaomiao.nosensordetected-notification" - case tryAgainLater = "com.loopkit.libremiaomiao.glucoseNotAvailableTryAgainLater-notification" - case sensorChange = "com.loopkit.libremiaomiao.sensorchange-notification" - case invalidSensor = "com.loopkit.libremiaomiao.invalidsensor-notification" - case lowBattery = "com.loopkit.libremiaomiao.lowbattery-notification" - case sensorExpire = "com.loopkit.libremiaomiao.SensorExpire-notification" - case noBridgeSelected = "com.loopkit.libremiaomiao.noBridgeSelected-notification" - case invalidChecksum = "com.loopkit.libremiaomiao.invalidChecksum-notification" - case calibrationOngoing = "com.loopkit.libremiaomiao.calibration-notification" - case libre2directFinishedSetup = "com.loopkit.libremiaomiao.libre2direct-notification" - } - - public static var shouldRequestCriticalPermissions = false - - // don't touch this please - public static var criticalAlarmsEnabled = false - - - - private static func vibrate(times: Int=3) { - guard times >= 0 else { - return - } - - - AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate) { - vibrate(times: times - 1) - } - } - - public static func GlucoseUnitIsSupported(unit: HKUnit) -> Bool { - [HKUnit.milligramsPerDeciliter, HKUnit.millimolesPerLiter].contains(unit) - } - - private static func requestCriticalNotificationPermissions() { - logger.debug("\(#function) called") - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.badge, .sound, .alert, .criticalAlert]) { (granted, error) in - if granted { - logger.debug("\(#function) was granted") - UNUserNotificationCenter.current().getNotificationSettings { settings in - logPermissions(settings) - criticalAlarmsEnabled = settings.criticalAlertSetting == .enabled - } - } else { - logger.debug("\(#function) failed because of error: \(String(describing: error))") - } - - } - - } - - private static func logPermissions(_ settings: UNNotificationSettings, caller: String = #function) { - - logger.debug("\(caller): alarms allowed: \(String(describing:settings.authorizationStatus)). Critical alarms allowed? \(String(describing:settings.criticalAlertSetting))") - - } - - public static func requestNotificationPermissionsIfNeeded() { - // We assume loop will request necessary "non-critical" permissions for us - // So we are only interested in the "critical" permissions here - - UNUserNotificationCenter.current().getNotificationSettings { settings in - criticalAlarmsEnabled = settings.criticalAlertSetting == .enabled - logPermissions(settings) - - if shouldRequestCriticalPermissions || NotificationHelperOverride.shouldOverrideRequestCriticalPermissions { - requestCriticalNotificationPermissions() - } - - } - } - - private static func ensureCanSendNotification(_ completion: @escaping () -> Void ) { - UNUserNotificationCenter.current().getNotificationSettings { settings in - guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else { - logger.debug("\(#function) failed, authorization denied") - return - } - logger.debug("\(#function) sending notification was allowed") - - completion() - } - } - - private static func addRequest(identifier: Identifiers, content: UNMutableNotificationContent, deleteOld: Bool = false, isCritical: Bool = false) { - let center = UNUserNotificationCenter.current() - - if isCritical && Self.criticalAlarmsEnabled { - logger.debug("\(#function) critical alarm created") - content.interruptionLevel = .critical - - let criticalVolume = UserDefaults.standard.mmCriticalAlarmsVolume < 60 ? 60 : UserDefaults.standard.mmCriticalAlarmsVolume - logger.debug("\(#function) setting criticalVolume to \(criticalVolume)%") - content.sound = .defaultCriticalSound(withAudioVolume: Float(criticalVolume / 100)) - } else { - logger.debug("\(#function) timesensitive alarm created") - content.interruptionLevel = .timeSensitive - } - - let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: nil) - - if deleteOld { - // Required since ios12+ have started to cache/group notifications - center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue]) - center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue]) - } - - center.add(request) { error in - if let error { - logger.debug("\(#function) unable to addNotificationRequest: \(error.localizedDescription)") - return - } - - logger.debug("\(#function) sending \(identifier.rawValue) notification") - } - } - -} - -// MARK: Sensor related notification sendouts -public extension NotificationHelper { - static func sendLibre2DirectFinishedSetupNotifcation() { - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.title = "Libre 2 Direct Setup Complete" - content.body = "Establishing initial connection can take up to 4 minutes. Keep your phone unlocked and Loop in the foreground while connecting" - - addRequest(identifier: .libre2directFinishedSetup, content: content) - } - } - - static func sendSensorNotDetectedNotificationIfNeeded(noSensor: Bool) { - guard UserDefaults.standard.mmAlertNoSensorDetected && noSensor else { - logger.debug("\(#function) Not sending noSensorDetected notification") - return - } - - sendSensorNotDetectedNotification() - } - - private static func sendSensorNotDetectedNotification() { - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.title = "No Sensor Detected" - content.body = "This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor" - - addRequest(identifier: .noSensorDetected, content: content) - } - } - - static func sendSensorChangeNotificationIfNeeded() { - guard UserDefaults.standard.mmAlertNewSensorDetected else { - logger.debug("\(#function) not sending sendSensorChange notification ") - return - } - sendSensorChangeNotification() - } - - static func sendSensorChangeNotification() { - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.title = "New Sensor Detected" - content.body = "Please wait up to 30 minutes before glucose readings are available!" - - addRequest(identifier: .sensorChange, content: content) - // content.sound = UNNotificationSound. - - } - } - - static func sendSensorTryAgainLaterNotification() { - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.title = "Invalid Glucose sample detected, try again later" - content.body = "Sensor might have temporarily stopped, fallen off or is too cold or too warm" - - addRequest(identifier: .tryAgainLater, content: content) - // content.sound = UNNotificationSound. - - } - } - - static func sendInvalidSensorNotificationIfNeeded(sensorData: SensorData) { - let isValid = sensorData.isLikelyLibre1FRAM && (sensorData.state == .starting || sensorData.state == .ready) - - guard UserDefaults.standard.mmAlertInvalidSensorDetected && !isValid else { - logger.debug("\(#function) not sending invalidSensorDetected notification") - return - } - - sendInvalidSensorNotification(sensorData: sensorData) - } - - enum CalibrationMessage: String { - case starting = "Calibrating sensor, please stand by!" - case noCalibration = "Could not calibrate sensor, check libreoopweb permissions and internet connection" - case invalidCalibrationData = "Could not calibrate sensor, invalid calibrationdata" - case success = "Success!" - } - - static func sendCalibrationNotification(_ calibrationMessage: CalibrationMessage) { - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.sound = .default - content.title = "Extracting calibrationdata from sensor" - content.body = calibrationMessage.rawValue - - addRequest(identifier: .calibrationOngoing, - content: content, - deleteOld: true) - } - } - - static func sendInvalidSensorNotification(sensorData: SensorData) { - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.title = "Invalid Sensor Detected" - - if !sensorData.isLikelyLibre1FRAM { - content.body = "Detected sensor seems not to be a libre 1 sensor!" - } else if !(sensorData.state == .starting || sensorData.state == .ready) { - content.body = "Detected sensor is invalid: \(sensorData.state.description)" - } - - content.sound = .default - - addRequest(identifier: .invalidSensor, content: content) - } - } - - private static var lastSensorExpireAlert: Date? - - static func sendSensorExpireAlertIfNeeded(minutesLeft: Double) { - guard UserDefaults.standard.mmAlertWillSoonExpire else { - logger.debug("\(#function) mmAlertWillSoonExpire toggle was not enabled, not sending expiresoon alarm") - return - } - - guard TimeInterval(minutes: minutesLeft) < TimeInterval(hours: 24) else { - logger.debug("\(#function) Sensor time left was more than 24 hours, not sending notification: \(minutesLeft.twoDecimals) minutes") - return - } - - let now = Date() - // only once per 6 hours - let min45 = 60.0 * 60 * 6 - - if let earlier = lastSensorExpireAlert { - if earlier.addingTimeInterval(min45) < now { - sendSensorExpireAlert(minutesLeft: minutesLeft) - lastSensorExpireAlert = now - } else { - logger.debug("\(#function) Sensor is soon expiring, but lastSensorExpireAlert was sent less than 6 hours ago, so aborting") - } - } else { - sendSensorExpireAlert(minutesLeft: minutesLeft) - lastSensorExpireAlert = now - } - } - - static func sendSensorExpireAlertIfNeeded(sensorData: SensorData) { - sendSensorExpireAlertIfNeeded(minutesLeft: Double(sensorData.minutesLeft)) - } - - private static func sendSensorExpireAlert(minutesLeft: Double) { - ensureCanSendNotification { - - let hours = minutesLeft == 0 ? 0 : round(minutesLeft/60) - - let dynamicText = hours <= 1 ? "minutes: \(minutesLeft.twoDecimals)" : "hours: \(hours.twoDecimals)" - - let content = UNMutableNotificationContent() - content.title = "Sensor Ending Soon" - content.body = "Current Sensor is Ending soon! Sensor Life left in \(dynamicText)" - - addRequest(identifier: .sensorExpire, content: content, deleteOld: true, isCritical: true) - } - } -} - -// MARK: - Notification sendout -public extension NotificationHelper { - - - static func sendNoTransmitterSelectedNotification() { - ensureCanSendNotification { - logger.debug("\(#function) sending NoTransmitterSelectedNotification") - - let content = UNMutableNotificationContent() - content.title = "No Libre Transmitter Selected" - content.body = "Delete CGMManager and start anew. Your libreoopweb credentials will be preserved" - - addRequest(identifier: .noBridgeSelected, content: content) - } - } - - static func sendInvalidChecksumIfDeveloper(_ sensorData: SensorData) { - guard UserDefaults.standard.dangerModeActivated else { - return - } - - if sensorData.hasValidCRCs { - return - } - - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.title = "Invalid libre checksum" - content.body = "Libre sensor was incorrectly read, CRCs were not valid" - - addRequest(identifier: .invalidChecksum, content: content) - } - } - - private static var glucoseNotifyCalledCount = 0 - - static func sendGlucoseNotificationIfNeeded(glucose: LibreGlucose, oldValue: LibreGlucose?, trend: GlucoseTrend?, battery: String?, glucoseFormatter: QuantityFormatter) { - glucoseNotifyCalledCount &+= 1 - - let shouldSendGlucoseAlternatingTimes = glucoseNotifyCalledCount != 0 && UserDefaults.standard.mmNotifyEveryXTimes != 0 - - let shouldSend = UserDefaults.standard.mmAlwaysDisplayGlucose || - (shouldSendGlucoseAlternatingTimes && glucoseNotifyCalledCount % UserDefaults.standard.mmNotifyEveryXTimes == 0) - - let schedules = UserDefaults.standard.glucoseSchedules - - let alarm = schedules?.getActiveAlarms(glucose.glucoseDouble) ?? .none - let isSnoozed = GlucoseScheduleList.isSnoozed() - - let shouldShowPhoneBattery = UserDefaults.standard.mmShowPhoneBattery - let transmitterBattery = UserDefaults.standard.mmShowTransmitterBattery && battery != nil ? battery : nil - - logger.debug("\(#function) glucose alarmtype is \(String(describing: alarm))") - // We always send glucose notifications when alarm is active, - // even if glucose notifications are disabled in the UI - - if shouldSend || alarm.isAlarming() { - sendGlucoseNotification(glucose: glucose, oldValue: oldValue, - glucoseFormatter: glucoseFormatter, - alarm: alarm, isSnoozed: isSnoozed, - trend: trend, showPhoneBattery: shouldShowPhoneBattery, - transmitterBattery: transmitterBattery) - } else { - logger.debug("\(#function) not sending glucose, shouldSend and alarmIsActive was false") - return - } - } - - private static func sendGlucoseNotification(glucose: LibreGlucose, oldValue: LibreGlucose?, - glucoseFormatter: QuantityFormatter, - alarm: GlucoseScheduleAlarmResult = .none, - isSnoozed: Bool = false, - trend: GlucoseTrend?, - showPhoneBattery: Bool = false, - transmitterBattery: String?) { - let content = UNMutableNotificationContent() - let glucoseDesc = glucoseFormatter.string(from: glucose.quantity)! - var titles = [String]() - var body = [String]() - var body2 = [String]() - - var isCritical = false - switch alarm { - case .none: - titles.append("Glucose") - case .low: - titles.append("LOWALERT!") - isCritical = true - case .high: - titles.append("HIGHALERT!") - isCritical = true - } - - if isSnoozed { - titles.append("(Snoozed)") - } else if alarm.isAlarming() { - content.sound = .default - - if Features.glucoseAlarmsAlsoCauseVibration { - vibrate() - } - - } - titles.append(glucoseDesc) - - body.append("Glucose: \(glucoseDesc)") - - if let oldValue { - let diff = glucose.glucoseDouble - oldValue.glucoseDouble - if diff >= 0 { - body.append("+") - } - body.append( glucoseFormatter.string(from: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: diff))!) - } - - if let trend = trend?.localizedDescription { - body.append("\(trend)") - } - - if showPhoneBattery { - if !UIDevice.current.isBatteryMonitoringEnabled { - UIDevice.current.isBatteryMonitoringEnabled = true - } - - let battery = Double(UIDevice.current.batteryLevel * 100 ).roundTo(places: 1) - body2.append("Phone: \(battery)%") - } - - if let transmitterBattery { - body2.append("Transmitter: \(transmitterBattery)") - } - - // these are texts that naturally fit on their own line in the body - var body2s = "" - if !body2.isEmpty { - body2s = "\n" + body2.joined(separator: "\n") - } - - content.title = titles.joined(separator: " ") - content.body = body.joined(separator: ", ") + body2s - addRequest(identifier: .glucocoseNotifications, - content: content, - deleteOld: true, isCritical: isCritical && !isSnoozed) - } - - private static var lastBatteryWarning: Date? - - static func sendLowBatteryNotificationIfNeeded(device: LibreTransmitterMetadata) { - guard UserDefaults.standard.mmAlertLowBatteryWarning else { - logger.debug("\(#function) mmAlertLowBatteryWarning toggle was not enabled, not sending low notification") - return - } - - if let battery = device.battery, battery > 20 { - logger.debug("\(#function) device battery is \(battery), not sending low notification") - return - - } - - let now = Date() - // only once per mins minute - let mins = 60.0 * 120 - if let earlierplus = lastBatteryWarning?.addingTimeInterval(mins) { - if earlierplus < now { - sendLowBatteryNotification(batteryPercentage: device.batteryString, - deviceName: device.name) - lastBatteryWarning = now - } else { - logger.debug("\(#function) Device battery is running low, but lastBatteryWarning Notification was sent less than 45 minutes ago, aborting. earlierplus: \(earlierplus), now: \(now)") - } - } else { - sendLowBatteryNotification(batteryPercentage: device.batteryString, - deviceName: device.name) - lastBatteryWarning = now - } - } - - private static func sendLowBatteryNotification(batteryPercentage: String, deviceName: String) { - ensureCanSendNotification { - let content = UNMutableNotificationContent() - content.title = "Low Battery" - content.body = "Battery is running low (\(batteryPercentage)), consider charging your \(deviceName) device as soon as possible" - content.sound = .default - - addRequest(identifier: .lowBattery, content: content) - } - } - -} diff --git a/LibreTransmitter/NotificationHelperOverride.swift b/LibreTransmitter/NotificationHelperOverride.swift deleted file mode 100644 index 80daa2c..0000000 --- a/LibreTransmitter/NotificationHelperOverride.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// NotificationHelperOverride.swift -// LibreTransmitter -// -// Created by Bjørn Inge Berg on 16/01/2023. -// Copyright © 2023 Mark Wilson. All rights reserved. -// - -import Foundation -enum NotificationHelperOverride { - static var shouldOverrideRequestCriticalPermissions : Bool { - // if you want LibreTransmitter to try upgrading to critical notifications, change this - false - } -} diff --git a/LibreTransmitter/Observables/AlarmStatus.swift b/LibreTransmitter/Observables/AlarmStatus.swift deleted file mode 100644 index 92addf1..0000000 --- a/LibreTransmitter/Observables/AlarmStatus.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// AlarmStatus.swift -// LibreTransmitter -// -// Created by LoopKit Authors on 09/07/2021. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import Foundation -public class AlarmStatus: ObservableObject, Equatable, Hashable { - @Published public var isAlarming = false - @Published public var glucoseScheduleAlarmResult = GlucoseScheduleAlarmResult.none - - public static func ==(lhs: AlarmStatus, rhs: AlarmStatus) -> Bool { - lhs.isAlarming == rhs.isAlarming && lhs.glucoseScheduleAlarmResult == rhs.glucoseScheduleAlarmResult - } - - static public func createNew() -> AlarmStatus { - AlarmStatus() - } -} diff --git a/LibreTransmitterUI/LibreTransmitterManager+UI.swift b/LibreTransmitterUI/LibreTransmitterManager+UI.swift index aa50375..d8609a4 100644 --- a/LibreTransmitterUI/LibreTransmitterManager+UI.swift +++ b/LibreTransmitterUI/LibreTransmitterManager+UI.swift @@ -18,10 +18,26 @@ struct LibreLifecycleProgress: DeviceLifecycleProgress { var progressState: LoopKit.DeviceLifecycleProgressState } +struct LibreStatusHighlight: DeviceStatusHighlight { + var localizedMessage: String + var imageName: String + var state: DeviceStatusHighlightState +} + +struct LibreStatusBadge: DeviceStatusBadge { + var image: UIImage? + var state: DeviceStatusBadgeState +} + extension LibreTransmitterManagerV3: CGMManagerUI { public var cgmStatusBadge: DeviceStatusBadge? { - nil + switch sensorLifecycle { + case .expired: + return LibreStatusBadge(image: UIImage(systemName: "exclamationmark.triangle.fill"), state: .critical) + default: + return nil + } } public static func setupViewController(bluetoothProvider: BluetoothProvider, displayGlucosePreference: DisplayGlucosePreference, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, prefersToSkipUserInteraction: Bool) -> SetupUIResult @@ -49,7 +65,6 @@ extension LibreTransmitterManagerV3: CGMManagerUI { notifyDelete: wantToTerminateNotifier, notifyReset: wantToResetCGMManagerNotifier, notifyReconnect:wantToRestablishConnectionNotifier, - alarmStatus: self.alarmStatus, pairingService: self.pairingService, bluetoothSearcher: self.bluetoothSearcher ) @@ -99,7 +114,22 @@ extension LibreTransmitterManagerV3: CGMManagerUI { } public var cgmStatusHighlight: DeviceStatusHighlight? { - nil + switch sensorLifecycle { + case .warmup: + return LibreStatusHighlight(localizedMessage: LocalizedString("Sensor\nWarmup", comment: "Status highlight message for sensor warmup"), imageName: "clock", state: .normalCGM) + case .active: + return nil + case .expired: + return LibreStatusHighlight(localizedMessage: LocalizedString("Sensor\nExpired", comment: "Status highlight message for expired sensor"), imageName: "clock", state: .critical) + case .signalLost: + return LibreStatusHighlight(localizedMessage: LocalizedString("Signal\nLoss", comment: "Status highlight message for signal loss"), imageName: "exclamationmark.circle.fill", state: .warning) + case .failed: + return LibreStatusHighlight(localizedMessage: LocalizedString("Replace\nSensor", comment: "Status highlight message for a failed sensor"), imageName: "exclamationmark.circle.fill", state: .critical) + case .unactivated: + return LibreStatusHighlight(localizedMessage: LocalizedString("Sensor\nNot Detected", comment: "Status highlight message for a sensor that is not detected"), imageName: "exclamationmark.circle.fill", state: .critical) + case .noSensor: + return nil + } } public var cgmLifecycleProgress: DeviceLifecycleProgress? { @@ -109,20 +139,25 @@ extension LibreTransmitterManagerV3: CGMManagerUI { // We could show 0 here, but UX-wise it's probably wiser to not do so return nil } - - let minutesLeft = Double(self.sensorInfoObservable.sensorMinutesLeft) - - // This matches the manufacturere's app where it displays a notification when sensor has less than 3 days left - if TimeInterval(minutes: minutesLeft) < TimeInterval(hours: 24*3) { - let progress = self.sensorInfoObservable.calculateProgress() - if TimeInterval(minutes: minutesLeft) < TimeInterval(hours: 24) { - return LibreLifecycleProgress(percentComplete: progress, progressState: .warning) + + switch sensorLifecycle { + case let .warmup(progress, _): + return LibreLifecycleProgress(percentComplete: progress, progressState: .warning) + case let .active(remaining, total): + // Mirrors G7SensorKit's cgmLifecycleProgress exactly: the bar only + // appears in the final 48h, .warning inside the final 24h, + // .normalCGM for the 24-48h stretch before that. + guard remaining < TimeInterval(hours: 48) else { + return nil } - return LibreLifecycleProgress(percentComplete: progress, progressState: .normalCGM) + let percent = 1 - (remaining / total) + let state: DeviceLifecycleProgressState = remaining < TimeInterval(hours: 24) ? .warning : .normalCGM + return LibreLifecycleProgress(percentComplete: percent, progressState: state) + case .expired: + return LibreLifecycleProgress(percentComplete: 1, progressState: .critical) + case .signalLost, .failed, .unactivated, .noSensor: + return nil } - - return nil - } } diff --git a/LibreTransmitterUI/Views/Settings/AlarmSettings/AlarmSettingsView.swift b/LibreTransmitterUI/Views/Settings/AlarmSettings/AlarmSettingsView.swift deleted file mode 100644 index 0467ba4..0000000 --- a/LibreTransmitterUI/Views/Settings/AlarmSettings/AlarmSettingsView.swift +++ /dev/null @@ -1,380 +0,0 @@ -// -// AlarmSettingsView.swift -// LibreTransmitterUI -// -// Created by LoopKit Authors on 11/05/2021. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import SwiftUI -import HealthKit - -private func systemImage(_ name:String) -> some View { - Image(systemName: name) - .resizable() - .interpolation(.high) - .scaledToFit() - .frame(width: 40) -} - -class AlarmScheduleState: ObservableObject, Identifiable, Hashable { - - var id = UUID() - - @Published var lowmgdl: Double = 72 - @Published var highmgdl: Double = 180 - @Published var enabled: Bool? = false - - @Published var alarmDateComponents: AlarmTimeCellExternalState = AlarmTimeCellExternalState() - - public func setLowAlarm(forUnit unit: HKUnit, lowAlarm: Double) { - - if unit == HKUnit.millimolesPerLiter { - self.lowmgdl = lowAlarm * 18 - return - } - - self.lowmgdl = lowAlarm - } - - public func getLowAlarm(forUnit unit: HKUnit) -> Double { - - if unit == HKUnit.millimolesPerLiter { - return (lowmgdl / 18).roundTo(places: 1) - } - - return lowmgdl - - } - - public func setHighAlarm(forUnit unit: HKUnit, highAlarm: Double) { - - if unit == HKUnit.millimolesPerLiter { - self.highmgdl = highAlarm * 18 - return - } - - self.highmgdl = highAlarm - } - - public func getHighAlarm(forUnit unit: HKUnit) -> Double { - - if unit == HKUnit.millimolesPerLiter { - return (highmgdl / 18).roundTo(places: 1) - } - return highmgdl - - } - -} - -class AlarmSettingsState: ObservableObject { - @Published var schedules: [AlarmScheduleState] = [] - - static private func setDateComponentState(_ state: AlarmScheduleState) { - if state.alarmDateComponents.startComponents == nil { - state.alarmDateComponents.startComponents = DateComponents(hour: 0, minute: 0) - } - - if state.alarmDateComponents.endComponents == nil { - state.alarmDateComponents.endComponents = DateComponents(hour: 0, minute: 0) - } - - let from = state.alarmDateComponents.startComponents!.ToTimeString() - let to = state.alarmDateComponents.endComponents!.ToTimeString() - - state.alarmDateComponents.componentsAsText = "\(from) - \(to)" - - } - - // this is just to be able to use old serialized schedules from uikit version - // i.e. we want to be drop in compatible - static func loadState() -> AlarmSettingsState { - - guard let storedState = UserDefaults.standard.glucoseSchedules, storedState.schedules.count > 0 else { - print("stored state for alarms was empty") - let newState = AlarmSettingsState() - for _ in (0.. StatusMessage? { - let legacyState = GlucoseScheduleList() - for newStateSchedule in self.schedules { - let glucoseSchedule = GlucoseSchedule() - glucoseSchedule.enabled = newStateSchedule.enabled - // view is using wrapper binding to store these values so should be safe - glucoseSchedule.lowAlarm = newStateSchedule.lowmgdl - glucoseSchedule.highAlarm = newStateSchedule.highmgdl - glucoseSchedule.from = newStateSchedule.alarmDateComponents.startComponents - glucoseSchedule.to = newStateSchedule.alarmDateComponents.endComponents - - legacyState.schedules.append(glucoseSchedule) - - } - let result = legacyState.validateGlucoseSchedules() - switch result { - case .success: - - print("glucose schedule valid: \(String(describing: legacyState))") - UserDefaults.standard.glucoseSchedules = legacyState - case .error(let description): - print("Could not save glucose schedules, validation failed: \(description)") - return StatusMessage(title: "Error", message: description) - } - - return nil - - } -} - -struct AlarmDateRow: View { - @ObservedObject var schedule: AlarmScheduleState - @State var tag: Int - @Binding var subviewSelection: Int? - - var body: some View { - - HStack(alignment: .center) { - NavigationLink(destination: CustomDataPickerView().environmentObject(schedule.alarmDateComponents), - tag: tag, - selection: $subviewSelection) { - Group { - systemImage("clock.arrow.2.circlepath") - .frame(maxWidth: 50, alignment: .leading) - TextField("Active from - to ", text: Binding(get: { "\(schedule.alarmDateComponents.componentsAsText)" }, - set: { schedule.alarmDateComponents.componentsAsText = $0 })) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .disableAutocorrection(true) - .keyboardType(.decimalPad) - .border(Color(UIColor.separator)) - .disabled(true) - .frame(minWidth: 130, idealWidth: 130, maxWidth: .infinity, alignment: .center) - }.onTapGesture { - print("cheduleActivationRow tapped") - subviewSelection = tag - self.hideKeyboardPreIos16() - - } - - } - - Toggle("", isOn: Binding( - get: { - schedule.enabled == true - }, - set: { - if $0 != schedule.enabled { - schedule.enabled = $0 - } - } - )) - .frame(maxWidth: 50, alignment: .trailing) - - } - } -} - -struct AlarmLowRow: View { - @ObservedObject var schedule: AlarmScheduleState - var glucoseUnit: HKUnit - var glucoseUnitDesc: String - - var errorReporter: FormErrorState - - @FocusState private var isInputFocused: Bool - var body: some View { - HStack(alignment: .center) { - - systemImage("arrowtriangle.down.circle") - .frame(maxWidth: 50, alignment: .leading) - Text(LocalizedString("Low", comment: "Text describing Low glucose label in alarmsettingsview")) - .frame(maxWidth: 100, alignment: .leading) - .onTapGesture { - isInputFocused.toggle() - } - Spacer() - - NumericTextField(description: "glucose", showDescription: false, - numericValue: Binding( - get: { - schedule.getLowAlarm(forUnit: glucoseUnit) - - }, - set: { - schedule.setLowAlarm(forUnit: glucoseUnit, lowAlarm: $0) - }), formErrorState: errorReporter) - .focused($isInputFocused) - - Text("\(glucoseUnitDesc)") - .font(.footnote) - .frame(maxWidth: 100, alignment: .trailing) - - } - .onTapGesture { - self.hideKeyboardPreIos16() - } - } -} - -struct AlarmHighRow: View { - @ObservedObject var schedule: AlarmScheduleState - var glucoseUnit: HKUnit - var glucoseUnitDesc: String - - var errorReporter: FormErrorState - @FocusState private var isInputFocused: Bool - var body: some View { - HStack(alignment: .center) { - - systemImage( "arrowtriangle.up.circle") - .frame(maxWidth: 50, alignment: .leading) - Text(LocalizedString("High", comment: "Text describing High glucose label in alarmsettingsview")) - .frame(maxWidth: 100, alignment: .leading) - .onTapGesture { - isInputFocused.toggle() - } - Spacer() - - NumericTextField(description: "glucose", showDescription: false, - numericValue: Binding( - get: { schedule.getHighAlarm(forUnit: glucoseUnit) }, - set: { - schedule.setHighAlarm(forUnit: glucoseUnit, highAlarm: $0) - - }), formErrorState: errorReporter) - .focused($isInputFocused) - Text("\(glucoseUnitDesc)") - .font(.footnote) - .frame(maxWidth: 100, alignment: .trailing) - - } - .onTapGesture { - self.hideKeyboardPreIos16() - } - } -} - -struct AlarmSettingsView: View { - - private(set) var glucoseUnit: HKUnit - - var glucoseUnitDesc: String { - // "mmol/L" - glucoseUnit.localizedShortUnitString - } - - @State private var presentableStatus: StatusMessage? - @StateObject var alarmState = AlarmSettingsState.loadState() - @State private var subviewSelection: Int? - - @State private var authSuccess = false - - // Set this to true to require system authentication - // for accessing the alarm section - @State private var requiresAuthentication = Features.alarmSettingsViewRequiresAuthentication - - var body: some View { - erasedWithKeyboardDismissal(list) - .alert(item: $presentableStatus) { status in - Alert(title: Text(status.title), message: Text(status.message), dismissButton: .default(Text("Got it!"))) - } - .navigationBarTitle("Alarm Settings") - .onAppear { - if requiresAuthentication && !authSuccess { - self.authenticate { success in - print("got authentication response: \(success)") - authSuccess = success - } - } - - } - .disabled(requiresAuthentication ? !authSuccess : false) - } - - func erasedWithKeyboardDismissal(_ view: any View) -> AnyView { - if #available(iOS 16.0, *) { - return AnyView(view.scrollDismissesKeyboard(.immediately)) - } - - return AnyView(view) - } - - @StateObject var errorReporter = FormErrorState() - - var list: some View { - - List { - ForEach(Array(alarmState.schedules.enumerated()), id: \.1) { i, schedule in - Section(header: Text(LocalizedString("Schedule ", comment: "Text describing schedule in alarmsettingsview") + "\(i+1)")) { - AlarmDateRow(schedule: schedule, tag: i, subviewSelection: $subviewSelection) - AlarmLowRow(schedule: schedule, glucoseUnit: glucoseUnit, glucoseUnitDesc: glucoseUnitDesc, errorReporter: errorReporter) - AlarmHighRow(schedule: schedule, glucoseUnit: glucoseUnit, glucoseUnitDesc: glucoseUnitDesc, errorReporter: errorReporter) - - }.onTapGesture { - self.hideKeyboardPreIos16() - } - - } - - Section { - Button("Save") { - saveButtonAction() - }.buttonStyle(BlueButtonStyle()) - } - } - .listStyle(InsetGroupedListStyle()) - - } - - func saveButtonAction() { - print("tapped save schedules") - if errorReporter.hasAnyError { - presentableStatus = StatusMessage(title: "Error", message: "Some ui element was incorrectly specified") - return - } - if let error = alarmState.trySaveState() { - presentableStatus = error - } else { - presentableStatus = StatusMessage(title: "Success", message: "Schedules were saved successfully!") - } - - } - -} - -struct AlarmSettingsView_Previews: PreviewProvider { - static var previews: some View { - AlarmSettingsView(glucoseUnit: .millimolesPerLiter) - } -} diff --git a/LibreTransmitterUI/Views/Settings/AlarmSettings/CriticalAlarmsVolumeView.swift b/LibreTransmitterUI/Views/Settings/AlarmSettings/CriticalAlarmsVolumeView.swift deleted file mode 100644 index cf76b4f..0000000 --- a/LibreTransmitterUI/Views/Settings/AlarmSettings/CriticalAlarmsVolumeView.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// CriticalAlarmsVolumeView.swift -// LibreTransmitterUI -// -// Created by LoopKit Authors on 29/01/2023. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import SwiftUI - -struct CriticalAlarmsVolumeView: View { - - private var intVolume : Int { - Int(mmCriticalAlarmsVolume) - } - @State private var isEditing = false - - private enum Key: String { - case mmCriticalAlarmsVolume = "com.loopkit.libreCriticalAlarmsVolume" - } - - @AppStorage(Key.mmCriticalAlarmsVolume.rawValue) var mmCriticalAlarmsVolume: Double = 60 - - var body: some View { - List { - Section(header: Text("Critical alarm volume"), footer: Text("Critical alarms will always be sent with volume at minimum 60%")) { - Slider( - value: $mmCriticalAlarmsVolume, - in: 60...100, - step: 5, - onEditingChanged: { editing in - isEditing = editing - } - ) - Text("\(intVolume)%") - .foregroundColor(isEditing ? .red : .blue) - - } - } - } -} - -struct CriticalAlarmsVolumeView_Previews: PreviewProvider { - static var previews: some View { - CriticalAlarmsVolumeView() - } -} diff --git a/LibreTransmitterUI/Views/Settings/AlarmSettings/CustomDataPickerView.swift b/LibreTransmitterUI/Views/Settings/AlarmSettings/CustomDataPickerView.swift deleted file mode 100644 index 9827358..0000000 --- a/LibreTransmitterUI/Views/Settings/AlarmSettings/CustomDataPickerView.swift +++ /dev/null @@ -1,260 +0,0 @@ -// -// CustomDataPickerView.swift -// LibreTransmitterUI -// -// Created by LoopKit Authors on 28/04/2021. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import SwiftUI - -protocol CustomDataPickerDelegate: AnyObject { - func pickerDidPickValidRange() - -} - -class AlarmTimeCellExternalState: ObservableObject, Identifiable, Hashable { - - var id = UUID() - - @Published var start: Int = 0 - @Published var end: Int = 0 - - // These will be auto populøated - // when the start and end properties above change - @Published var startComponents: DateComponents? - @Published var endComponents: DateComponents? - - @Published var componentsAsText: String = "" - -} - -// handle parts of alarmsettingsview's state (=externalstate) -struct CustomDataPickerView: View { - private var startComponentTimes: [DateComponents] - private var endComponentTimes: [DateComponents] - - private var startTimes = [String]() - private var endTimes = [String]() - - @Environment(\.presentationMode) var presentationMode - @EnvironmentObject var externalState: AlarmTimeCellExternalState - - @State var externalStateCopy: AlarmTimeCellExternalState = AlarmTimeCellExternalState() - - public weak var delegate: CustomDataPickerDelegate? - - private func popView() { - self.presentationMode.wrappedValue.dismiss() - } - - static func defaultTimeArray() -> [DateComponents] { - var arr = [DateComponents]() - - for hr in 0...23 { - for min in 0 ..< 2 { - var components = DateComponents() - components.hour = hr - components.minute = min == 1 ? 30 : 0 - arr.append(components) - } - } - var components = DateComponents() - components.hour = 0 - components.minute = 0 - arr.append(components) - - return arr - } - - private func callDelegate() { - delegate?.pickerDidPickValidRange() - } - - private func verifyRange() { - - // This can be simplified but decided not to do so - // because the intention becomes more clear - - var isok: Bool - - if externalState.start == 0 || externalState.end == 0 { - isok = true - } else { - if externalState.start > externalState.end { - isok = false - } else if externalState.end < externalState.start { - isok = false - } else { - isok = true - } - } - - print("is ok? \(isok)") - if isok { - updateTextualState() - callDelegate() - popView() - - } else { - presentableStatus = .init(title: "Interval error", message: "Selected time interval was incorrectly specified") - } - - } - - var pickers: some View { - HStack { - Picker("", selection: $externalState.start, - content: { - ForEach(startTimes.indices, id: \.self) { i in - Text("\(startTimes[i])").tag(i) - } - } - ) - // .border(Color.green) - - .zIndex(10) - .frame(width: 100) - .clipped() - .labelsHidden() - - Text(LocalizedString("To ", comment: "Very short text describing separation between start and end datetimes")) - - Picker("", selection: $externalState.end, - content: { - ForEach(endTimes.indices, id: \.self) { i in - Text("\(endTimes[i])").tag(i) - } - } - ) - // .border(Color.red) - .zIndex(11) - .frame(width: 100) - .clipped() - .labelsHidden() - - // } - - .navigationBarBackButtonHidden(true) - .navigationBarItems( - leading: - Button("Cancel") { - print("cancel button pressed, restoring state...") - restoreAlarmExternalState() - popView() - - }.accentColor(.red), - trailing: - Button("Save") { - print("Save button pressed...") - verifyRange() - } - .disabled(saveButtonDisabled) - .accentColor(.red) - - ) - } - - } - - @State private var presentableStatus: StatusMessage? - @State private var saveButtonDisabled = true - - private func updateTextualState(_ shouldDelete: Bool = false) { - if shouldDelete { - externalState.componentsAsText = "" - return - } - if let p1 = externalState.startComponents?.ToTimeString(), let p2 = externalState.endComponents?.ToTimeString() { - externalState.componentsAsText = "\(p1) - \(p2)" - } - } - - var body: some View { - - pickers - .pickerStyle(InlinePickerStyle()) - .onChange(of: externalState.start, perform: { value in - print("selectedtart changed to \(value)") - externalState.startComponents = startComponentTimes[value] - saveButtonDisabled = false - - }) - .onChange(of: externalState.end, perform: { value in - print("selectedEnd changed to \(value)") - externalState.endComponents = endComponentTimes[value] - saveButtonDisabled = false - - }) - .onAppear { - // this could potentially fail with out of bounds but we trust our parent view! - externalState.startComponents = startComponentTimes[externalState.start] - externalState.endComponents = endComponentTimes[externalState.end] - updateTextualState() - - copyAlarmExternalState() - - } - .alert(item: $presentableStatus) { status in - Alert(title: Text(status.title), message: Text(status.message), dismissButton: .default(Text("Got it!"))) - } - - } - - // decided against uding nscoding with copy() here - private func copyAlarmExternalState() { - externalStateCopy = AlarmTimeCellExternalState() - /* - var id = UUID() - - @Published var start : Int = 0 - @Published var end : Int = 0 - - // These will be auto populøated - // when the start and end properties above change - @Published var startComponents : DateComponents? = nil - @Published var endComponents : DateComponents? = nil - - @Published var componentsAsText : String = ""**/ - externalStateCopy.id = externalState.id - externalStateCopy.start = externalState.start - externalStateCopy.end = externalState.end - externalStateCopy.startComponents = externalState.startComponents - externalStateCopy.endComponents = externalStateCopy.endComponents - externalStateCopy.componentsAsText = externalState.componentsAsText - - } - - private func restoreAlarmExternalState() { - externalState.id = externalStateCopy.id - externalState.start = externalStateCopy.start - externalState.end = externalStateCopy.end - externalState.startComponents = externalStateCopy.startComponents - externalStateCopy.endComponents = externalStateCopy.endComponents - externalState.componentsAsText = externalStateCopy.componentsAsText - - } - - init() { - startComponentTimes = Self.defaultTimeArray() - endComponentTimes = Self.defaultTimeArray() - - // string representations of the datecomponents arrays - - for component in startComponentTimes { - startTimes.append(component.ToTimeString(wantsAMPM: Date.LocaleWantsAMPM)) - } - - for component in endComponentTimes { - endTimes.append(component.ToTimeString(wantsAMPM: Date.LocaleWantsAMPM)) - - } - - } -} - -struct CustomDataPickerView_Previews: PreviewProvider { - static var previews: some View { - CustomDataPickerView().environmentObject(AlarmTimeCellExternalState()) - } -} diff --git a/LibreTransmitterUI/Views/Settings/NotificationSettingsView.swift b/LibreTransmitterUI/Views/Settings/NotificationSettingsView.swift deleted file mode 100644 index 33c73a6..0000000 --- a/LibreTransmitterUI/Views/Settings/NotificationSettingsView.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// NotificationSettingsView.swift -// LibreTransmitterUI -// -// Created by LoopKit Authors on 27/05/2021. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import SwiftUI -import Combine -import LibreTransmitter -import HealthKit -import LoopKitUI - -struct NotificationSettingsView: View { - @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference - - @State private var presentableStatus: StatusMessage? - - private let glucoseSegments = [HKUnit.millimolesPerLiter, HKUnit.milligramsPerDeciliter] - private lazy var glucoseSegmentStrings = self.glucoseSegments.map({ $0.localizedShortUnitString }) - - private enum Key: String { - // case glucoseSchedules = "com.loopkit.libreglucoseschedules" - - case mmAlwaysDisplayGlucose = "com.loopkit.libreAlwaysDisplayGlucose" - case mmNotifyEveryXTimes = "com.loopkit.libreNotifyEveryXTimes" - case mmAlertLowBatteryWarning = "com.loopkit.libreLowBatteryWarning" - case mmAlertInvalidSensorDetected = "com.loopkit.libreInvalidSensorDetected" - // case mmAlertalarmNotifications - case mmAlertNewSensorDetected = "com.loopkit.libreNewSensorDetected" - case mmAlertNoSensorDetected = "com.loopkit.libreNoSensorDetected" - - case mmAlertSensorSoonExpire = "com.loopkit.libreAlertSensorSoonExpire" - - case mmShowPhoneBattery = "com.loopkit.libreShowPhoneBattery" - case mmShowTransmitterBattery = "com.loopkit.libreShowTransmitterBattery" - - // handle specially: - case mmGlucoseUnit = "com.loopkit.libreGlucoseUnit" - - } - - @AppStorage(Key.mmAlwaysDisplayGlucose.rawValue) var mmAlwaysDisplayGlucose: Bool = true - @AppStorage(Key.mmNotifyEveryXTimes.rawValue) var mmNotifyEveryXTimes: Int = 0 - @AppStorage(Key.mmShowPhoneBattery.rawValue) var mmShowPhoneBattery: Bool = false - @AppStorage(Key.mmShowTransmitterBattery.rawValue) var mmShowTransmitterBattery: Bool = true - - @AppStorage(Key.mmAlertLowBatteryWarning.rawValue) var mmAlertLowBatteryWarning: Bool = true - @AppStorage(Key.mmAlertInvalidSensorDetected.rawValue) var mmAlertInvalidSensorDetected: Bool = true - @AppStorage(Key.mmAlertNewSensorDetected.rawValue) var mmAlertNewSensorDetected: Bool = true - @AppStorage(Key.mmAlertNoSensorDetected.rawValue) var mmAlertNoSensorDetected: Bool = true - @AppStorage(Key.mmAlertSensorSoonExpire.rawValue) var mmAlertSensorSoonExpire: Bool = true - - - // especially handled mostly for backward compat - @AppStorage(Key.mmGlucoseUnit.rawValue) var mmGlucoseUnit: String = "" - - @State var notifyErrorState = FormErrorState() - - @State private var favoriteGlucoseUnit = 0 - - static let formatter = NumberFormatter() - - var glucoseVisibilitySection : some View { - Section(header: Text(LocalizedString("Glucose Notification visibility", comment: "Text describing header for notification visibility in notificationsettingsview")) ) { - Toggle(LocalizedString("Always Notify Glucose", comment: "Text describing always notify glucose option in notificationsettingsview"), isOn: $mmAlwaysDisplayGlucose) - - HStack { - Text(LocalizedString("Notify per reading", comment: "Text describing option for letting user choose notifying for every reading, every second reading etc")) - TextField("", value: $mmNotifyEveryXTimes, formatter: Self.formatter) - .multilineTextAlignment(.center) - .disabled(true) - .frame(minWidth: 15, maxWidth: 60) - .textFieldStyle(RoundedBorderTextFieldStyle()) - Stepper("Value", value: $mmNotifyEveryXTimes, in: 0...9) - .labelsHidden() - - }.clipped() - - - Toggle("Adds Transmitter Battery", isOn: $mmShowTransmitterBattery) - - - } - } - - var additionalNotificationsSection : some View { - Section(header: Text(LocalizedString("Additional notification types", comment: "Text describing heading for additional notification types for third party transmitters"))) { - Toggle("Low battery", isOn: $mmAlertLowBatteryWarning) - Toggle("Invalid sensor", isOn: $mmAlertInvalidSensorDetected) - Toggle("Sensor change", isOn: $mmAlertNewSensorDetected) - Toggle("Sensor not found", isOn: $mmAlertNoSensorDetected) - Toggle("Sensor expires soon", isOn: $mmAlertSensorSoonExpire) - - } - } - - /*var miscSection : some View { - Section(header: Text("Misc")) { - HStack { - Text("Unit override") - Picker(selection: $favoriteGlucoseUnit, label: Text("Unit override")) { - Text(HKUnit.millimolesPerLiter.localizedShortUnitString).tag(0) - Text(HKUnit.milligramsPerDeciliter.localizedShortUnitString).tag(1) - } - .pickerStyle(SegmentedPickerStyle()) - .clipped() - } - } - }*/ - - var body: some View { - List { - - glucoseVisibilitySection - additionalNotificationsSection - - } - .listStyle(InsetGroupedListStyle()) - .alert(item: $presentableStatus) { status in - Alert(title: Text(status.title), message: Text(status.message), dismissButton: .default(Text("Got it!"))) - } - - .navigationBarTitle("Notification") - - } - -} - -struct NotificationSettingsView_Previews: PreviewProvider { - static var previews: some View { - NotificationSettingsView() - } -} diff --git a/LibreTransmitterUI/Views/Settings/SettingsView.swift b/LibreTransmitterUI/Views/Settings/SettingsView.swift index ac1b6e1..3f0083d 100644 --- a/LibreTransmitterUI/Views/Settings/SettingsView.swift +++ b/LibreTransmitterUI/Views/Settings/SettingsView.swift @@ -69,7 +69,6 @@ struct SettingsView: View { @ObservedObject private var notifyReconnect: GenericObservableObject @State private var presentableStatus: StatusMessage? - @ObservedObject var alarmStatus: LibreTransmitter.AlarmStatus @State private var showingDestructQuestion = false // @State private var showingExporter = false @@ -86,7 +85,6 @@ struct SettingsView: View { notifyDelete: GenericObservableObject, notifyReset: GenericObservableObject, notifyReconnect: GenericObservableObject, - alarmStatus: LibreTransmitter.AlarmStatus, pairingService: SensorPairingProtocol, bluetoothSearcher: BluetoothSearcher) { @@ -97,15 +95,10 @@ struct SettingsView: View { self.notifyDelete = notifyDelete self.notifyReset = notifyReset self.notifyReconnect = notifyReconnect - self.alarmStatus = alarmStatus self.pairingService = pairingService self.bluetoothSearcher = bluetoothSearcher } - private var glucoseUnit: HKUnit { - displayGlucosePreference.unit - } - static let formatter = NumberFormatter() // no navigationview necessary when running inside a uihostingcontroller @@ -114,7 +107,6 @@ struct SettingsView: View { var body: some View { List { headerSection - snoozeSection measurementSection if let date = glucoseMeasurement.predictionDate, let prediction = glucoseMeasurement.prediction { Section(header: Text(LocalizedString("Last Blood Sugar prediction", comment: "Text describing header for Blood Sugar prediction section"))) { @@ -144,19 +136,6 @@ struct SettingsView: View { } } - var snoozeSection: some View { - Section { - NavigationLink(destination: SnoozeView(isAlarming: $alarmStatus.isAlarming, activeAlarms: $alarmStatus.glucoseScheduleAlarmResult)) { - Image(systemName: "pause.circle.fill") - .font(.system(size: 22)) - .foregroundColor(.blue) - Text(LocalizedString("Pause Glucose alarms", comment: "Text for pausing glucose alarms")).frame(alignment: .center) - .foregroundColor(.blue) - - } - } - } - var measurementSection : some View { var glucoseText: String = "" var glucoseDateText: String = "" @@ -259,26 +238,9 @@ struct SettingsView: View { var advancedSection: some View { Section(header: Text(LocalizedString("Configuration", comment: "Text describing header for advanced settings section"))) { - // these subviews don't really need to be notified once glucose unit changes - // so we just pass glucoseunit directly on init - NavigationLink(destination: AlarmSettingsView(glucoseUnit: self.glucoseUnit)) { - SettingsItem(title: "Alarms") - } - - if NotificationHelper.criticalAlarmsEnabled { - NavigationLink(destination: CriticalAlarmsVolumeView()) { - SettingsItem(title: "Critical Alarms volume") - } - } - NavigationLink(destination: GlucoseSettingsView()) { SettingsItem(title: "Glucose Settings") } - - NavigationLink(destination: NotificationSettingsView()) { - SettingsItem(title: "Notifications") - } - } } @@ -463,6 +425,6 @@ struct SettingsView: View { struct SettingsOverview_Previews: PreviewProvider { static var previews: some View { - NotificationSettingsView() + GlucoseSettingsView() } } diff --git a/LibreTransmitterUI/Views/Settings/SnoozeView.swift b/LibreTransmitterUI/Views/Settings/SnoozeView.swift deleted file mode 100644 index a637b60..0000000 --- a/LibreTransmitterUI/Views/Settings/SnoozeView.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// TestView.swift -// MiaomiaoClientUI -// -// Created by LoopKit Authors on 15/10/2020. -// Copyright © 2020 LoopKit Authors. All rights reserved. -// - -import LibreTransmitter -import SwiftUI - -struct SnoozeView: View { - - var pickerTimes: [TimeInterval] = ({ - pickerTimesArray() - - })() - - var formatter: DateComponentsFormatter = ({ - var f = DateComponentsFormatter() - f.allowsFractionalUnits = false - f.unitsStyle = .full - return f - - })() - - func formatInterval(_ interval: TimeInterval) -> String { - formatter.string(from: interval)! - } - - @Binding var isAlarming: Bool - @Binding var activeAlarms: LibreTransmitter.GlucoseScheduleAlarmResult - - static func pickerTimesArray() -> [TimeInterval] { - var arr = [TimeInterval]() - - let mins10 = 0.166_67 - let mins20 = mins10 * 2 - let mins30 = mins10 * 3 - // let mins40 = mins10 * 4 - - for hr in 0..<2 { - for min in [0.0, mins20, mins20 * 2] { - arr.append(TimeInterval(hours: Double(hr) + min)) - } - } - for hr in 2..<4 { - for min in [0.0, mins30] { - arr.append(TimeInterval(hours: Double(hr) + min)) - } - } - - for hr in 4...8 { - arr.append(TimeInterval(hours: Double(hr))) - } - - return arr - } - - func getSnoozeDescription() -> String { - var snoozeDescription = "" - var celltext = "" - - switch activeAlarms { - case .high: - celltext = "High Glucose Alarm active" - case .low: - celltext = "Low Glucose Alarm active" - case .none: - celltext = "No Glucose Alarm active" - } - - if let until = GlucoseScheduleList.snoozedUntil { - snoozeDescription = "snoozing until \(until.description(with: .current))" - } else { - snoozeDescription = "not snoozing" - } - - return [celltext, snoozeDescription].joined(separator: ", ") - } - - @State private var selectedInterval = 0 - @State private var snoozeDescription = "nothing to see here" - - var snoozeButton: some View { - VStack(alignment: .leading) { - Button(action: { - print("snooze from testview clicked") - let interval = pickerTimes[selectedInterval] - let snoozeFor = formatter.string(from: interval)! - let untilDate = Date() + interval - UserDefaults.standard.snoozedUntil = untilDate < Date() ? nil : untilDate - print("will snooze for \(snoozeFor) until \(untilDate.description(with: .current))") - snoozeDescription = getSnoozeDescription() - }, label: { - Text(LocalizedString("Click to Snooze Alerts", comment: "Text describing click to snooze label in snoozeview")) - .padding() - }) - } - - } - - var snoozePicker: some View { - VStack { - Picker(selection: $selectedInterval, label: Text("Strength")) { - ForEach(0 ..< pickerTimes.count, id: \.self) { - Text(formatInterval(self.pickerTimes[$0])) - } - } - - .scaledToFill() - .pickerStyle(.wheel) - } - - } - - var snoozeDesc : some View { - VStack(alignment: .leading) { - Text(snoozeDescription) - } - } - - var body: some View { - Form { - Section { - Text(snoozeDescription).lineLimit(nil) - snoozePicker - snoozeButton - } - } - .onAppear { - snoozeDescription = getSnoozeDescription() - } - } -} - -struct TestView_Previews: PreviewProvider { - static var previews: some View { - SnoozeView(isAlarming: .constant(true), activeAlarms: .constant(.none)) - } -} diff --git a/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift b/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift index 057f3c7..7cea70b 100644 --- a/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift +++ b/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift @@ -220,7 +220,6 @@ struct BluetoothSelection: View { init(cancelNotifier: GenericObservableObject, saveNotifier: GenericObservableObject, searcher: BluetoothSearcher) { self.cancelNotifier = cancelNotifier self.saveNotifier = saveNotifier - LibreTransmitter.NotificationHelper.requestNotificationPermissionsIfNeeded() self.searcher = searcher } diff --git a/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift b/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift index cdf4303..00cdaa9 100644 --- a/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift +++ b/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift @@ -59,14 +59,11 @@ struct Libre2DirectSetup: View { do { try KeychainManager.standard.setLibreNativeCalibrationData(calibrationData) } catch { - NotificationHelper.sendCalibrationNotification(.invalidCalibrationData) return } // here we assume success, data is not changed, // and we trust that the remote endpoint returns correct data for the sensor - NotificationHelper.sendCalibrationNotification(.success) - UserDefaults.standard.calibrationMapping = CalibrationToSensorMapping(uuid: info.uuid, reverseFooterCRC: calibrationData.isValidForFooterWithReverseCRCs) } @@ -83,8 +80,6 @@ struct Libre2DirectSetup: View { SelectionState.shared.selectedStringIdentifier = nil print("Paired and set selected UID to: \(String(describing: SelectionState.shared.selectedUID?.hex))") saveNotifier.notify() - NotificationHelper.sendLibre2DirectFinishedSetupNotifcation() - } var cancelButton: some View { diff --git a/build.md b/build.md index 2e600be..505885e 100644 --- a/build.md +++ b/build.md @@ -3,51 +3,6 @@ NB! This project requires LoopWorkspace dev. You *must* use the workspace to bui ## Start with a clean LoopWorkspace based on dev * Download a fresh copy of LoopWorkspace dev, which now includes the LibreTransmitter module by default. - -## Give Loop Extra background permissions - The LibreTransmitter plugin will run as a part of Loop. If you want LibreTransmitter to be able to give vibrations for low/high glucose, this is a necessary step -* In Xcode, Open the Loop Project (not the LibreTransmitter project) in the navigator, go to "Signing & Capabilities", then under "background modes", select "Audio, AirPlay, and Picture in picture". This will allow Libretransmitter to use vibration when the phone is locked. -* It should look like this: ![Loop_xcodeproj](https://user-images.githubusercontent.com/442324/111884302-14777a80-89c1-11eb-9171-76ffcef2f345.jpg "Audio/Vibrate capability added into Loop For libretransmitter to work in background") -* In code, search for and set glucoseAlarmsAlsoCauseVibration to true - - -## Give Libretransmitter Critical Alerts permissions -Libretransmitter will by default send alarms as "timesensitve", appearing immediately on the lock screen. -If you mute or set your phone to do not disturb, you can potentially miss out on such alarms. -To remedy this, LibreTransmitter can be configured to try to upgrade any glucose alarms to "critical". -Critical alarms will sound even if your phone is set to to mute or "do not disturb" mode. - -For this to be possible, you will have to request special permissions from Apple. -This process is documented at https://stackoverflow.com/questions/66057840/ios-how-do-you-implement-critical-alerts-for-your-app-when-you-dont-have-an-en . -The linked article describes some necessary code changes, but the code changes mentioned there should be ignored as the necessary code changes are already in place for Libretransmitter. - -For critical alerts to function, a custom provisioning profile must be selected. This is typically the provisioning profile you get after a successful application to Apple - -It's worth mentioning again that those permissions must be given to Loop itself, not to the LibreTransmitter package. The -com.apple.developer.usernotifications.critical-alerts permission must be added to Loop/Loop.entitlement file in the Loop folder (not inside LibreTransmitter - -Next, choose one of these two methods to enable this feature: - - -### Method 1 -Using this method, only the LibreTransmitter cgm alarms can become critical - -You should only change the shouldOverrideRequestCriticalPermissions toggle in the NotificationHelperOverride.swift file to true, like this: - -```swift -enum NotificationHelperOverride { - static var shouldOverrideRequestCriticalPermissions : Bool { - // if you want LibreTransmitter to try upgrading to critical notifications, change this - true - } -} - -``` -### Method 2 -Using this method, both Loop pump, cgm alarms and LibreTransmitter alarms will become critical - -Go to the Loop Project (not target)→Build settings → Swift Compiler → Custom flags → Other swift flags section and edit the different configuration flags. Add the flag “CRITICAL_ALERTS_ENABLED” (without the quotes). - -## Build the LoopWorkspace +## Build the LoopWorkspace * In xcode, build the LoopWorkspace as normal diff --git a/readme.md b/readme.md index 8285498..fd02ae3 100644 --- a/readme.md +++ b/readme.md @@ -19,7 +19,6 @@ This is a https://github.com/loopkit/loop plugin for connecting to libresensors * Glucose values can be uploaded to nightscout automatically by Loop * Optional: Backfill the last 16 minutes of data (recommend to turn off) * Optional: Backfill the last 8 hours of data (15 minute cadence) -* Glucose data is smoothed to avoid noise, using a 5 point moving average filter * Official algorithm implements glucose prediction, to align cgm values with blood values; this feature is deliberately removed from this implementation * Glucose readout interval: 5 minutes * Glucose alarms