diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index b3277d955..131d2099b 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -37,6 +37,12 @@ 47D9BC952A78498500AB85B2 /* BgReadingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D9BC942A78498500AB85B2 /* BgReadingsDetailView.swift */; }; 47F8E95A2710255D00B8B02B /* ConstantsWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F8E9592710255C00B8B02B /* ConstantsWatchApp.swift */; }; 47FB28082636B04200042FFB /* StatisticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FB28072636B04200042FFB /* StatisticsManager.swift */; }; + 9D06FF5B2A76EB1600ECEA9B /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + 9D06FF602A76EC0000ECEA9B /* DiablyLib in Frameworks */ = {isa = PBXBuildFile; productRef = 9D06FF5F2A76EC0000ECEA9B /* DiablyLib */; }; + 9D06FF622A76EC0000ECEA9B /* OG in Frameworks */ = {isa = PBXBuildFile; productRef = 9D06FF612A76EC0000ECEA9B /* OG */; }; + 9D67DF752A6DBEDC009A15DD /* SettingsViewOpenGlückSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D67DF742A6DBEDC009A15DD /* SettingsViewOpenGlückSettingsViewModel.swift */; }; + 9D67DF7A2A6DC365009A15DD /* OpenGlückManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D67DF792A6DC365009A15DD /* OpenGlückManager.swift */; }; + 9D67DF7D2A6DC603009A15DD /* (null) in Frameworks */ = {isa = PBXBuildFile; }; CE1B2FE025D0264B00F642F5 /* LaunchScreen.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE1B2FD125D0264900F642F5 /* LaunchScreen.strings */; }; CE1B2FE125D0264B00F642F5 /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE1B2FD425D0264900F642F5 /* Main.strings */; }; D400F8032778BD8000B57648 /* TextsTreatmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D400F8022778BD8000B57648 /* TextsTreatmentsView.swift */; }; @@ -739,6 +745,8 @@ 47FB28072636B04200042FFB /* StatisticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsManager.swift; sourceTree = ""; }; 666E283826F7E54C00ACE4DF /* xDrip.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = xDrip.xcconfig; path = xdrip/xDrip.xcconfig; sourceTree = ""; }; 666E283926F7E54C00ACE4DF /* Version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Version.xcconfig; path = xdrip/Version.xcconfig; sourceTree = ""; }; + 9D67DF742A6DBEDC009A15DD /* SettingsViewOpenGlückSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsViewOpenGlückSettingsViewModel.swift"; sourceTree = ""; }; + 9D67DF792A6DC365009A15DD /* OpenGlückManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OpenGlückManager.swift"; sourceTree = ""; }; CE1B2FC825D0261500F642F5 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Alerts.strings; sourceTree = ""; }; CE1B2FCD25D0264900F642F5 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/AlertTypesSettingsView.strings; sourceTree = ""; }; CE1B2FCE25D0264900F642F5 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/WatlaaView.strings; sourceTree = ""; }; @@ -1600,10 +1608,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9D06FF622A76EC0000ECEA9B /* OG in Frameworks */, F81F3C4225D1D91300520946 /* CoreNFC.framework in Frameworks */, 470824D2297484B500C52317 /* SwiftCharts in Frameworks */, + 9D67DF7D2A6DC603009A15DD /* (null) in Frameworks */, 4779BCEE2974306300515714 /* ActionClosurable in Frameworks */, F821CF9722AE589E005C1E43 /* HealthKit.framework in Frameworks */, + 9D06FF602A76EC0000ECEA9B /* DiablyLib in Frameworks */, + 9D06FF5B2A76EB1600ECEA9B /* (null) in Frameworks */, 4779BCF42974308F00515714 /* PieCharts in Frameworks */, 4779BCF12974307700515714 /* CryptoSwift in Frameworks */, ); @@ -1652,6 +1664,14 @@ name = Frameworks; sourceTree = ""; }; + 9D67DF782A6DC339009A15DD /* OpenGlückManager */ = { + isa = PBXGroup; + children = ( + 9D67DF792A6DC365009A15DD /* OpenGlückManager.swift */, + ); + path = "OpenGlückManager"; + sourceTree = ""; + }; D4028CBE2774A4B900341476 /* Treatments */ = { isa = PBXGroup; children = ( @@ -2068,6 +2088,7 @@ F821CF9122ADB064005C1E43 /* HealthKit */, F8E51D5B2448D8A3001C9E5A /* Loop */, F821CF4F229BF43A005C1E43 /* NightScout */, + 9D67DF782A6DC339009A15DD /* OpenGlückManager */, F64039AE281C3F8D0051EFFE /* QuickActions */, F821CF9922AEF2DF005C1E43 /* Speak */, 47FB28052636AFE700042FFB /* Statistics */, @@ -2549,6 +2570,7 @@ 4752B4052635878E0081D551 /* SettingsViewStatisticsSettingsViewModel.swift */, F8E51D6824549E2C001C9E5A /* SettingsViewTraceSettingsViewModel.swift */, 47150A3F27F6211C00DB2994 /* SettingsViewTreatmentsSettingsViewModel.swift */, + 9D67DF742A6DBEDC009A15DD /* SettingsViewOpenGlückSettingsViewModel.swift */, ); path = SettingsViewModels; sourceTree = ""; @@ -3231,6 +3253,8 @@ 4779BCF02974307700515714 /* CryptoSwift */, 4779BCF32974308F00515714 /* PieCharts */, 470824D1297484B500C52317 /* SwiftCharts */, + 9D06FF5F2A76EC0000ECEA9B /* DiablyLib */, + 9D06FF612A76EC0000ECEA9B /* OG */, ); productName = xdrip; productReference = F8AC425A21ADEBD60078C348 /* xdrip.app */; @@ -3299,6 +3323,7 @@ 4779BCEC2974306300515714 /* XCRemoteSwiftPackageReference "ActionClosurable" */, 4779BCEF2974307700515714 /* XCRemoteSwiftPackageReference "CryptoSwift" */, 4779BCF22974308F00515714 /* XCRemoteSwiftPackageReference "PieCharts" */, + 9D06FF5E2A76EC0000ECEA9B /* XCRemoteSwiftPackageReference "OG" */, ); productRefGroup = F8AC425B21ADEBD60078C348 /* Products */; projectDirPath = ""; @@ -3588,6 +3613,7 @@ F821CF56229BF43A005C1E43 /* AlertKind.swift in Sources */, F85DC2ED21CFE2F500B9F74A /* BgReading+CoreDataProperties.swift in Sources */, F8A2BC0D25DB0B12001D1E78 /* Atom+BluetoothPeripheral.swift in Sources */, + 9D67DF752A6DBEDC009A15DD /* SettingsViewOpenGlückSettingsViewModel.swift in Sources */, F8F9723123A5915900C3F17D /* M5StackAuthenticateTXMessage.swift in Sources */, F8F9721223A5915900C3F17D /* FirmwareVersionTxMessage.swift in Sources */, F80D916824F7086D006840B5 /* Libre2BluetoothPeripheralViewModel.swift in Sources */, @@ -3708,6 +3734,7 @@ F8CB59C22738206D00BA199E /* DexcomGlucoseDataTxMessage.swift in Sources */, F8C97854242AA70D00A09483 /* MiaoMiao+CoreDataProperties.swift in Sources */, F8F9720A23A5915900C3F17D /* DexcomTransmitterOpCode.swift in Sources */, + 9D67DF7A2A6DC365009A15DD /* OpenGlückManager.swift in Sources */, F8AC425E21ADEBD60078C348 /* AppDelegate.swift in Sources */, F8A2BC0825DB09BE001D1E78 /* Atom+CoreDataProperties.swift in Sources */, F8F1671927288FC6001AA3D8 /* DexcomSessionStartRxMessage.swift in Sources */, @@ -4799,7 +4826,7 @@ CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4827,7 +4854,7 @@ CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4916,6 +4943,14 @@ kind = branch; }; }; + 9D06FF5E2A76EC0000ECEA9B /* XCRemoteSwiftPackageReference "OG" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/callms/OG"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.65; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -4939,6 +4974,16 @@ package = 4779BCF22974308F00515714 /* XCRemoteSwiftPackageReference "PieCharts" */; productName = PieCharts; }; + 9D06FF5F2A76EC0000ECEA9B /* DiablyLib */ = { + isa = XCSwiftPackageProductDependency; + package = 9D06FF5E2A76EC0000ECEA9B /* XCRemoteSwiftPackageReference "OG" */; + productName = DiablyLib; + }; + 9D06FF612A76EC0000ECEA9B /* OG */ = { + isa = XCSwiftPackageProductDependency; + package = 9D06FF5E2A76EC0000ECEA9B /* XCRemoteSwiftPackageReference "OG" */; + productName = OG; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved b/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved index 60ad48007..e26e1ca05 100644 --- a/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "19b3c3ceed117c5cc883517c4e658548315ba70b", - "version" : "1.6.0" + "revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f", + "version" : "1.7.2" + } + }, + { + "identity" : "og", + "kind" : "remoteSourceControl", + "location" : "https://github.com/callms/OG", + "state" : { + "revision" : "7324a2d1ce066f15e101a157bcc7cd2903004fec", + "version" : "1.0.74" } }, { @@ -24,7 +33,7 @@ "location" : "https://github.com/paulplant/PieCharts", "state" : { "branch" : "master", - "revision" : "3d57b133b491c9be000aeeffc0c6db6cfc5af560" + "revision" : "15128eb4d1126dcdca4a5b665cece6be9f5dac91" } }, { diff --git a/xdrip/Constants/ConstantsLog.swift b/xdrip/Constants/ConstantsLog.swift index 20633ab3d..a52aa198e 100644 --- a/xdrip/Constants/ConstantsLog.swift +++ b/xdrip/Constants/ConstantsLog.swift @@ -97,6 +97,9 @@ enum ConstantsLog { /// healthkit manager static let categoryHealthKitManager = "HealthKitManager " + /// openglück + static let categoryOpenGlückManager = "OpenGlückManager " + /// SettingsViewHealthKitSettingsViewModel static let categorySettingsViewHealthKitSettingsViewModel = "SettingsViewHealthKitSettingsViewModel" diff --git a/xdrip/Extensions/UserDefaults.swift b/xdrip/Extensions/UserDefaults.swift index 02e00f185..ba5f6fb2b 100644 --- a/xdrip/Extensions/UserDefaults.swift +++ b/xdrip/Extensions/UserDefaults.swift @@ -174,6 +174,13 @@ extension UserDefaults { /// should readings be stored in healthkit, true or false case storeReadingsInHealthkit = "storeReadingsInHealthkit" + // OpenGlück + case openGlückEnabled = "openGlückEnabled" + case openGlückUploadEnabled = "openGlückUploadEnabled" + case openGlückHostname = "openGlückHostname" + case openGlückToken = "openGlückToken" + case timeStampLatestOpenGlückBgReading = "openGlückLastUpload" + // Speak readings /// speak readings @@ -1285,6 +1292,58 @@ extension UserDefaults { } } + // MARK: OpenGlück Settings + + /// is OpenGlück enabled? + @objc dynamic var openGlückEnabled: Bool { + get { + return bool(forKey: Key.openGlückEnabled.rawValue) + } + set { + set(newValue, forKey: Key.openGlückEnabled.rawValue) + } + } + + /// is OpenGlück upload enabled? + @objc dynamic var openGlückUploadEnabled: Bool { + get { + return bool(forKey: Key.openGlückUploadEnabled.rawValue) + } + set { + set(newValue, forKey: Key.openGlückUploadEnabled.rawValue) + } + } + + /// OpenGlück hostname + @objc dynamic var openGlückHostname: String? { + get { + return string(forKey: Key.openGlückHostname.rawValue) + } + set { + set(newValue, forKey: Key.openGlückHostname.rawValue) + } + } + + /// OpenGlück token + @objc dynamic var openGlückToken: String? { + get { + return string(forKey: Key.openGlückToken.rawValue) + } + set { + set(newValue, forKey: Key.openGlückToken.rawValue) + } + } + + /// OpenGlück last upload + @objc dynamic var timeStampLatestOpenGlückBgReading: Date? { + get { + return object(forKey: Key.timeStampLatestOpenGlückBgReading.rawValue) as? Date + } + set { + set(newValue, forKey: Key.timeStampLatestOpenGlückBgReading.rawValue) + } + } + // MARK: Speak Settings /// should readings be spoken or not diff --git a/xdrip/Managers/Alerts/AlertManager.swift b/xdrip/Managers/Alerts/AlertManager.swift index 62abd73c4..9d0236b0f 100644 --- a/xdrip/Managers/Alerts/AlertManager.swift +++ b/xdrip/Managers/Alerts/AlertManager.swift @@ -38,6 +38,9 @@ public class AlertManager:NSObject { /// playSound instance private var soundPlayer:SoundPlayer? + /// openGlück manager instance + private var openGlückManager:OpenGlückManager? + /// snooze parameters private var snoozeParameters = [SnoozeParameters]() @@ -63,7 +66,7 @@ public class AlertManager:NSObject { // MARK: - initializer - init(coreDataManager:CoreDataManager, soundPlayer:SoundPlayer?) { + init(coreDataManager:CoreDataManager, soundPlayer:SoundPlayer?, openGlückManager:OpenGlückManager?) { // initialize properties self.bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager) self.alertTypesAccessor = AlertTypesAccessor(coreDataManager: coreDataManager) @@ -71,6 +74,7 @@ public class AlertManager:NSObject { self.calibrationsAccessor = CalibrationsAccessor(coreDataManager: coreDataManager) self.sensorsAccessor = SensorsAccessor(coreDataManager: coreDataManager) self.soundPlayer = soundPlayer + self.openGlückManager = openGlückManager self.uNUserNotificationCenter = UNUserNotificationCenter.current() self.coreDataManager = coreDataManager @@ -669,18 +673,36 @@ public class AlertManager:NSObject { let notificationRequest = UNNotificationRequest(identifier: alertKind.notificationIdentifier(), content: content, trigger: trigger) // Add Request to User Notification Center - uNUserNotificationCenter.add(notificationRequest) { (error) in - if let error = error { - trace("Unable to Add Notification Request %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .error, error.localizedDescription) + let lastBgReadingTimestamp = lastBgReading?.timeStamp + Task { + // check if current alert can be dismissed by third-party manager + switch alertKind { + case .low: + // ask openglück + if let lastBgReadingTimestamp, let openGlückManager, await openGlückManager.shouldDismissLow(at: lastBgReadingTimestamp) { + trace("OpenGlück asked to dismiss a low notification", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info) + return + } + default: + // no possible dismissal + break + } + uNUserNotificationCenter.add(notificationRequest) { (error) in + if let error = error { + trace("Unable to Add Notification Request %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .error, error.localizedDescription) + } } } // snooze default period, to avoid that alert goes off every minute for Libre 2, except if it's a delayed alert (for delayed alerts it looks a bit risky to me) if delayInSecondsToUse == 0 { - - trace("in checkAlert, snoozing alert %{public}@ for %{public}@ minutes", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), ConstantsAlerts.defaultDelayBetweenAlertsOfSameKindInMinutes.description) - - getSnoozeParameters(alertKind: alertKind).snooze(snoozePeriodInMinutes: ConstantsAlerts.defaultDelayBetweenAlertsOfSameKindInMinutes) + if alertKind == .low || alertKind == .verylow { + trace("in checkAlert, deliberately NOT snoozing alert %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging()) + } else { + trace("in checkAlert, snoozing alert %{public}@ for %{public}@ minutes", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), ConstantsAlerts.defaultDelayBetweenAlertsOfSameKindInMinutes.description) + + getSnoozeParameters(alertKind: alertKind).snooze(snoozePeriodInMinutes: ConstantsAlerts.defaultDelayBetweenAlertsOfSameKindInMinutes) + } } diff --git "a/xdrip/Managers/OpenGl\303\274ckManager/OpenGl\303\274ckManager.swift" "b/xdrip/Managers/OpenGl\303\274ckManager/OpenGl\303\274ckManager.swift" new file mode 100644 index 000000000..ef30af55b --- /dev/null +++ "b/xdrip/Managers/OpenGl\303\274ckManager/OpenGl\303\274ckManager.swift" @@ -0,0 +1,218 @@ +import Foundation +import OG +import os + +public class OpenGlückManager: NSObject, OpenGlückSyncClientDelegate { + // MARK: - public properties + + // MARK: - private properties + + /// to solve problem that sometemes UserDefaults key value changes is triggered twice for just one change + private let keyValueObserverTimeKeeper: KeyValueObserverTimeKeeper = .init() + + /// for logging + private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryOpenGlückManager) + + /// reference to coredatamanager + private var coreDataManager: CoreDataManager + + /// reference to BgReadingsAccessor + private var bgReadingsAccessor: BgReadingsAccessor + + /// is OpenGlück fully initiazed or not, that includes checking if healthkit is available, created successfully bloodGlucoseType, user authorized - value will get changed + private var openGlückInitialized = false + + /// reference to the OpenGlück client, should be used only if we're sure OpenGlück is supported on the device + private var openGlückClient: OpenGlückClient? + private var openGlückSyncClient: OpenGlückSyncClient? + + /// dismisses low notifications 30m after low record + private let dismissLowAfter: TimeInterval = 30 * 60 // 30m + private var lastLowRecordAt: Date? = nil + + // MARK: - intialization + + init(coreDataManager: CoreDataManager) { + // initialize non optional private properties + self.coreDataManager = coreDataManager + bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager) + + // call super.init + super.init() + + // listen for changes to userdefaults + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.openGlückEnabled.rawValue, options: .new, context: nil) + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.openGlückUploadEnabled.rawValue, options: .new, context: nil) + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.openGlückHostname.rawValue, options: .new, context: nil) + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.openGlückToken.rawValue, options: .new, context: nil) + + // call initializeOpenGlück, set openGlückInitialized according to result of initialization + openGlückInitialized = initializeOpenGlück() + + // do first store + storeBgReadings() + } + + // MARK: - private functions + + /// checks if OpenGlück enabled, creates client + /// - returns: + /// - result which indicates if initialize was successful or not + /// + /// the return value of the function does not depend on UserDefaults.standard.openGlückEnabled - this setting needs to be verified each time there's an new reading to store + private func initializeOpenGlück() -> Bool { + openGlückClient = nil + openGlückSyncClient = nil + guard UserDefaults.standard.openGlückEnabled, let openGlückHostname = UserDefaults.standard.openGlückHostname, let openGlückToken = UserDefaults.standard.openGlückToken else { return false } + + openGlückClient = OpenGlückClient(hostname: openGlückHostname, token: openGlückToken, target: "xdripswift") + openGlückSyncClient = OpenGlückSyncClient() + openGlückSyncClient!.delegate = self + + // all checks ok , return true + return true + } + + let intervalBetweenHistoricRecords: TimeInterval = 5 * 60 // 5m + let historicScanTipoffInterval: TimeInterval = 20 * 60 // 20m + private func splitRecordsByHistoricScan(_ readings: [BgReading]) -> ([BgReading], [BgReading]) { + guard !readings.isEmpty else { return ([], []); } + let historicScanTipoffDate = Date().addingTimeInterval(-historicScanTipoffInterval) + print("historicScanTipoffDate=\(historicScanTipoffDate)") + let readings = readings.sorted(by: { $0.timeStamp < $1.timeStamp }) + let earliest = readings.first!.timeStamp + let latest = readings.last!.timeStamp + let base = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: earliest)! + var usedTimeStamps: Set = Set() + var d = base + var historics = [BgReading]() + while d < latest { + if let match = readings + .filter({ $0.timeStamp <= historicScanTipoffDate }) + .filter({ $0.timeStamp > d }).first, match.timeStamp.timeIntervalSince(d) < intervalBetweenHistoricRecords, !usedTimeStamps.contains(match.timeStamp) { + usedTimeStamps.insert(match.timeStamp) + historics.append(match) + } + d = d.addingTimeInterval(intervalBetweenHistoricRecords) + } + let lastHistoricAt = historics.map { $0.timeStamp }.max() ?? historicScanTipoffDate + let scans = readings.filter { $0.timeStamp > lastHistoricAt } + return (historics, scans) + } + + /// stores latest readings in healthkit, only if HK supported, authorized, enabled in settings + public func storeBgReadings() { + // TODO: + // healthkit setting must be on, and healthkit must be initialized successfully + if !UserDefaults.standard.openGlückEnabled || !openGlückInitialized { return } + + guard let openGlückClient = openGlückClient else { return } + + // get readings to store, limit to 15 = maximum 1 week - just to avoid a huge array is being returned here, applying minimumTimeBetweenTwoReadingsInMinutes filter + let bgReadingsToStore = bgReadingsAccessor.getLatestBgReadings(limit: 2016, fromDate: UserDefaults.standard.timeStampLatestOpenGlückBgReading, forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false).filter(minimumTimeBetweenTwoReadingsInMinutes: 1, lastConnectionStatusChangeTimeStamp: nil, timeStampLastProcessedBgReading: UserDefaults.standard.timeStampLatestOpenGlückBgReading) + + let loadLastRecordsSince = Date().addingTimeInterval(-86400) + let bgRecentRecords = bgReadingsAccessor.getLatestBgReadings(limit: nil, fromDate: loadLastRecordsSince, forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false).filter(minimumTimeBetweenTwoReadingsInMinutes: 1, lastConnectionStatusChangeTimeStamp: nil, timeStampLastProcessedBgReading: loadLastRecordsSince) + let (historics, scans) = splitRecordsByHistoricScan(bgRecentRecords) + print("Historic", historics.map { "\($0.timeStamp) \($0.calculatedValue)"}.joined(separator: "\n")) + print("Scans", scans.map { "\($0.timeStamp) \($0.calculatedValue)"}.joined(separator: "\n")) + // NOTE: when Libre smoothing is enabled, we kind of lose scan records and only have one available; if we don't want this + // we should store scan readings and re-upload them later -- though not sure this is a feature we'd want + + // reupload at least 4 historic records + scans + let uploadHistoricAfter = (UserDefaults.standard.timeStampLatestOpenGlückBgReading ?? Date().addingTimeInterval(-86400)).addingTimeInterval(-historicScanTipoffInterval) + let glucoseRecordsToUpload: [OpenGlückGlucoseRecord] = ( + historics.map ({ + OpenGlückGlucoseRecord(timestamp: $0.timeStamp, mgDl: Int(round($0.calculatedValue)), recordType: "historic") + }) + scans.map ({ + OpenGlückGlucoseRecord(timestamp: $0.timeStamp, mgDl: Int(round($0.calculatedValue)), recordType: "scan") + }) + ).filter { + $0.timestamp >= uploadHistoricAfter + } + let modelName = "xdripswift" + let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? "(unknown-ifv)" + let instantGlucoseRecords = bgReadingsToStore.map { bgReading in + OpenGlückInstantGlucoseRecord(timestamp: bgReading.timeStamp, mgDl: Int(round(bgReading.calculatedValue)), modelName: modelName, deviceId: deviceId) + } + Task { + if UserDefaults.standard.openGlückUploadEnabled { + if !glucoseRecordsToUpload.isEmpty { + do { + let timeStampLastReadingToUpload = glucoseRecordsToUpload.map { $0.timestamp }.max()! + let device = OpenGlückDevice(modelName: modelName, deviceId: deviceId) + _ = try await openGlückClient.upload(currentCgmProperties: CgmCurrentDeviceProperties(hasRealTime: true, realTimeInterval: 60), device: device, glucoseRecords: glucoseRecordsToUpload) + UserDefaults.standard.timeStampLatestOpenGlückBgReading = timeStampLastReadingToUpload + } catch { + trace("Could not upload to OpenGlück", log: self.log, category: ConstantsLog.categoryOpenGlückManager, type: .error, error.localizedDescription) + } + } + } else { + if !bgReadingsToStore.isEmpty { + let timeStampLastReadingToUpload = instantGlucoseRecords.map { $0.timestamp }.max()! + do { + let result = try await openGlückClient.upload(instantGlucoseRecords: instantGlucoseRecords) + if result.success { + UserDefaults.standard.timeStampLatestOpenGlückBgReading = timeStampLastReadingToUpload + } + } catch { + return + } + } + } + } + } + + public func getClient() -> OpenGlückClient { + return openGlückClient! + } + + /// whether we should dismiss a low notification — typically this happens when a low has been recorded in the last 30 minutes + public func shouldDismissLow(at: Date) async -> Bool { + guard let openGlückSyncClient else { return false } + + if let lastLowRecordAt, at >= lastLowRecordAt && at <= lastLowRecordAt.addingTimeInterval(dismissLowAfter) { + trace("shouldDismissLow => true (cache)", log: self.log, category: ConstantsLog.categoryOpenGlückManager, type: .info, "Low bg reading at \(at) should be dismissed because of a low at \(lastLowRecordAt)") + return true + } + + // get the latest low + do { + let latest = try await openGlückSyncClient.getLastData() + if let lows = latest.lowRecords { + lastLowRecordAt = lows + .filter { !$0.deleted } + .filter {at >= $0.timestamp && at <= $0.timestamp.addingTimeInterval(dismissLowAfter) } + .sorted(by: { $0.timestamp > $1.timestamp }) + .first?.timestamp + } + if let lastLowRecordAt, at >= lastLowRecordAt && at <= lastLowRecordAt.addingTimeInterval(dismissLowAfter) { + trace("shouldDismissLow => true (sync)", log: self.log, category: ConstantsLog.categoryOpenGlückManager, type: .info, "Low bg reading at \(at) should be dismissed because of a low at \(lastLowRecordAt)") + return true + } + } catch { + // ignore errors + trace("Caught error while syncing OpenGlück, ignoring", log: self.log, category: ConstantsLog.categoryOpenGlückManager, type: .error, error.localizedDescription) + } + + return false + } + + // MARK: - observe function + + /// when UserDefaults storeReadingsInHealthkitAuthorized or storeReadingsInHealthkit changes, then reinitialize the property openGlückInitialized + override public func observeValue(forKeyPath keyPath: String?, of _: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) { + if let keyPath = keyPath { + if let keyPathEnum = UserDefaults.Key(rawValue: keyPath) { + switch keyPathEnum { + case UserDefaults.Key.openGlückEnabled, UserDefaults.Key.openGlückUploadEnabled, UserDefaults.Key.openGlückHostname, UserDefaults.Key.openGlückToken: + openGlückInitialized = initializeOpenGlück() + storeBgReadings() + + default: + break + } + } + } + } +} diff --git a/xdrip/Texts/TextsSettingsView.swift b/xdrip/Texts/TextsSettingsView.swift index 936700a52..b125420d9 100644 --- a/xdrip/Texts/TextsSettingsView.swift +++ b/xdrip/Texts/TextsSettingsView.swift @@ -253,6 +253,36 @@ class Texts_SettingsView { return NSLocalizedString("settingsviews_healthkit", tableName: filename, bundle: Bundle.main, value: "Write Data to Apple Health?", comment: "healthkit settings, literally 'healthkit'") }() + // MARK: - Section OpenGlück + + static let sectionTitleOpenGlück: String = { + return NSLocalizedString("settingsviews_sectiontitleopenglück", tableName: filename, bundle: Bundle.main, value: "OpenGlück", comment: "openglück settings, section title") + }() + + static let labelOpenGlückEnabled = { + return NSLocalizedString("settingsviews_openGlückEnabled", tableName: filename, bundle: Bundle.main, value: "Upload To OpenGlück?", comment: "openglück settings, is it enabled") + }() + + static let labelOpenGlückUploadEnabled = { + return NSLocalizedString("settingsviews_openGlückUploadEnabled", tableName: filename, bundle: Bundle.main, value: "Upload Historic/Scan?", comment: "openglück settings, is it enabled to upload historic and scan records") + }() + + static let labelOpenGlückHostname = { + return NSLocalizedString("settingsviews_openGlückHostname", tableName: filename, bundle: Bundle.main, value: "Hostname:", comment: "openglück settings, hostname") + }() + + static let labelOpenGlückToken = { + return NSLocalizedString("settingsviews_openGlückToken", tableName: filename, bundle: Bundle.main, value: "Token:", comment: "openglück settings, token") + }() + + static let giveOpenGlückHostname = { + return NSLocalizedString("settingsviews_openGlückHostname", tableName: filename, bundle: Bundle.main, value: "Enter OpenGlück Hostname", comment: "openglück settings, pop up that asks user to enter openglück hostname") + }() + + static let giveOpenGlückToken = { + return NSLocalizedString("settingsviews_openGlückToken", tableName: filename, bundle: Bundle.main, value: "Enter OpenGlück Token", comment: "openglück settings, pop up that asks user to enter openglück token") + }() + // MARK: - Section Dexcom Share static let sectionTitleDexcomShare: String = { diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 0d85df365..0a7c088b0 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -433,6 +433,9 @@ final class RootViewController: UIViewController, ObservableObject { /// healthkit manager instance private var healthKitManager:HealthKitManager? + /// OpenGlück manager instance + private var openGlückManager:OpenGlückManager? + /// reference to activeSensor private var activeSensor:Sensor? @@ -903,7 +906,7 @@ final class RootViewController: UIViewController, ObservableObject { } } - // creates activeSensor, bgreadingsAccessor, calibrationsAccessor, NightScoutUploadManager, soundPlayer, dexcomShareUploadManager, nightScoutFollowManager, alertManager, healthKitManager, bgReadingSpeaker, bluetoothPeripheralManager, watchManager, housekeeper + // creates activeSensor, bgreadingsAccessor, calibrationsAccessor, NightScoutUploadManager, soundPlayer, dexcomShareUploadManager, nightScoutFollowManager, alertManager, healthKitManager, openGlückManager, bgReadingSpeaker, bluetoothPeripheralManager, watchManager, housekeeper private func setupApplicationData() { // setup Trace @@ -950,6 +953,9 @@ final class RootViewController: UIViewController, ObservableObject { // setup healthkitmanager healthKitManager = HealthKitManager(coreDataManager: coreDataManager) + // setup openGlückManager + openGlückManager = OpenGlückManager(coreDataManager: coreDataManager) + // setup bgReadingSpeaker bgReadingSpeaker = BGReadingSpeaker(sharedSoundPlayer: soundPlayer, coreDataManager: coreDataManager) @@ -1033,7 +1039,7 @@ final class RootViewController: UIViewController, ObservableObject { cgmTransmitterInfoChanged() // setup alertmanager - alertManager = AlertManager(coreDataManager: coreDataManager, soundPlayer: soundPlayer) + alertManager = AlertManager(coreDataManager: coreDataManager, soundPlayer: soundPlayer, openGlückManager: openGlückManager) // setup watchmanager watchManager = WatchManager(coreDataManager: coreDataManager) @@ -1310,6 +1316,8 @@ final class RootViewController: UIViewController, ObservableObject { nightScoutUploadManager?.uploadLatestBgReadings(lastConnectionStatusChangeTimeStamp: lastConnectionStatusChangeTimeStamp()) healthKitManager?.storeBgReadings() + + openGlückManager?.storeBgReadings() bgReadingSpeaker?.speakNewReading(lastConnectionStatusChangeTimeStamp: lastConnectionStatusChangeTimeStamp()) @@ -3376,6 +3384,10 @@ extension RootViewController:NightScoutFollowerDelegate { healthKitManager.storeBgReadings() } + if let openGlückManager = openGlückManager { + openGlückManager.storeBgReadings() + } + if let bgReadingSpeaker = bgReadingSpeaker { bgReadingSpeaker.speakNewReading(lastConnectionStatusChangeTimeStamp: lastConnectionStatusChangeTimeStamp()) } diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift index 91a00a11d..3f428eac8 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift @@ -56,6 +56,9 @@ final class SettingsViewController: UIViewController { /// healthkit case healthkit + /// openglück + case openGlück + /// store bg values in healthkit case speak @@ -100,6 +103,8 @@ final class SettingsViewController: UIViewController { return SettingsViewDexcomSettingsViewModel() case .healthkit: return SettingsViewHealthKitSettingsViewModel() + case .openGlück: + return SettingsViewOpenGlückSettingsViewModel() case .speak: return SettingsViewSpeakSettingsViewModel() case .M5stack: diff --git "a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewOpenGl\303\274ckSettingsViewModel.swift" "b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewOpenGl\303\274ckSettingsViewModel.swift" new file mode 100644 index 000000000..cf909b7a7 --- /dev/null +++ "b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewOpenGl\303\274ckSettingsViewModel.swift" @@ -0,0 +1,166 @@ +import UIKit + +fileprivate enum Setting:Int, CaseIterable { + case openGlückEnabled = 0 + case openGlückUploadEnabled = 3 + case openGlückHostname = 1 + case openGlückToken = 2 + +} + +/// conforms to SettingsViewModelProtocol for all OpenGlück settings in the first sections screen +class SettingsViewOpenGlückSettingsViewModel:SettingsViewModelProtocol { + + func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {} + + func storeUIViewController(uIViewController: UIViewController) {} + + func storeMessageHandler(messageHandler: ((String, String) -> Void)) { + // this ViewModel does need to send back messages to the viewcontroller asynchronously + } + + func completeSettingsViewRefreshNeeded(index: Int) -> Bool { + return true + } + + func isEnabled(index: Int) -> Bool { + + return true + + } + + func onRowSelect(index: Int) -> SettingsSelectedRowAction { + guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") } + + switch setting { + + case .openGlückEnabled, .openGlückUploadEnabled: + return SettingsSelectedRowAction.nothing + + case .openGlückHostname: + return SettingsSelectedRowAction.askText(title: Texts_SettingsView.labelOpenGlückHostname, message: Texts_SettingsView.giveOpenGlückHostname, keyboardType: UIKeyboardType.alphabet, text: UserDefaults.standard.openGlückHostname, placeHolder: nil, actionTitle: nil, cancelTitle: nil, actionHandler: {(hostname:String) in UserDefaults.standard.openGlückHostname = hostname.toNilIfLength0()}, cancelHandler: nil, inputValidator: nil) + + case .openGlückToken: + return SettingsSelectedRowAction.askText(title: Texts_SettingsView.labelOpenGlückToken, message: Texts_SettingsView.giveOpenGlückToken, keyboardType: UIKeyboardType.alphabet, text: UserDefaults.standard.openGlückToken, placeHolder: nil, actionTitle: nil, cancelTitle: nil, actionHandler: {(token:String) in UserDefaults.standard.openGlückToken = token.toNilIfLength0()}, cancelHandler: nil, inputValidator: nil) + + } + } + + func sectionTitle() -> String? { + return Texts_SettingsView.sectionTitleOpenGlück + } + + func numberOfRows() -> Int { + + if !UserDefaults.standard.openGlückEnabled { + return 1 + } + else { + return Setting.allCases.count + } + } + + func settingsRowText(index: Int) -> String { + guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") } + + switch setting { + case .openGlückEnabled: + return Texts_SettingsView.labelOpenGlückEnabled + case .openGlückUploadEnabled: + return Texts_SettingsView.labelOpenGlückUploadEnabled + case .openGlückHostname: + return Texts_SettingsView.labelOpenGlückHostname + case .openGlückToken: + return Texts_SettingsView.labelOpenGlückToken + } + } + + func accessoryType(index: Int) -> UITableViewCell.AccessoryType { + guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") } + + switch setting { + case .openGlückEnabled: + return UITableViewCell.AccessoryType.none + case .openGlückUploadEnabled: + return UITableViewCell.AccessoryType.none + case .openGlückHostname: + return UITableViewCell.AccessoryType.disclosureIndicator + case .openGlückToken: + return UITableViewCell.AccessoryType.disclosureIndicator + } + } + + func detailedText(index: Int) -> String? { + guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") } + + switch setting { + case .openGlückEnabled, .openGlückUploadEnabled: + return nil + case .openGlückHostname: + return UserDefaults.standard.openGlückHostname + case .openGlückToken: + return UserDefaults.standard.openGlückToken != nil ? "***********" : nil + } + } + + func uiView(index:Int) -> UIView? { + guard let setting = Setting(rawValue: index) else { fatalError("Unexpected Section") } + + switch setting { + case .openGlückEnabled: + return UISwitch(isOn: UserDefaults.standard.openGlückEnabled, action: {(isOn:Bool) in UserDefaults.standard.openGlückEnabled = isOn}) + case .openGlückUploadEnabled: + return UISwitch(isOn: UserDefaults.standard.openGlückUploadEnabled, action: {(isOn:Bool) in UserDefaults.standard.openGlückUploadEnabled = isOn}) + case .openGlückHostname: + return nil + case .openGlückToken: + return nil + } + } +} +/* +extension SettingsViewDexcomSettingsViewModel: TimeSchedule { + + func serviceName() -> String { + return "OpenGlück" + } + + func getSchedule() -> [Int] { + + var schedule = [Int]() + + if let scheduleInSettings = UserDefaults.standard.dexcomShareSchedule { + + schedule = scheduleInSettings.split(separator: "-").map({Int($0) ?? 0}) + + } + + return schedule + + } + + func storeSchedule(schedule: [Int]) { + + var scheduleToStore: String? + + for entry in schedule { + + if scheduleToStore == nil { + + scheduleToStore = entry.description + + } else { + + scheduleToStore = scheduleToStore! + "-" + entry.description + + } + + } + + UserDefaults.standard.dexcomShareSchedule = scheduleToStore + + } + + +}*/ +