diff --git a/xdrip/Extensions/UserDefaults.swift b/xdrip/Extensions/UserDefaults.swift index 9abbed0d0..02e00f185 100644 --- a/xdrip/Extensions/UserDefaults.swift +++ b/xdrip/Extensions/UserDefaults.swift @@ -291,7 +291,11 @@ extension UserDefaults { /// timestamp lastest reading shared with Loop case timeStampLatestLoopSharedBgReading = "timeStampLatestLoopSharedBgReading" - + + /// Loop sharing will be limited to just once every 5 minutes if true + case shareToLoopOnceEvery5Minutes = "shareToLoopOnceEvery5Minutes" + + // Trace /// should debug level logs be added in trace file or not, and also in NSLog case addDebugLevelLogsInTraceFileAndNSLog = "addDebugLevelLogsInTraceFileAndNSLog" @@ -1653,7 +1657,7 @@ extension UserDefaults { } } - /// timestamp lastest reading uploaded to NightScout + /// timestamp lastest reading shared with Loop via App Group var timeStampLatestLoopSharedBgReading:Date? { get { return object(forKey: Key.timeStampLatestLoopSharedBgReading.rawValue) as? Date @@ -1663,6 +1667,16 @@ extension UserDefaults { } } + /// Loop sharing will be limited to just once every 5 minutes if true - default false + var shareToLoopOnceEvery5Minutes: Bool { + get { + return bool(forKey: Key.shareToLoopOnceEvery5Minutes.rawValue) + } + set { + set(newValue, forKey: Key.shareToLoopOnceEvery5Minutes.rawValue) + } + } + // MARK: - ===== technical settings for testing ====== /// G6 factor 1 diff --git a/xdrip/Managers/Loop/LoopManager.swift b/xdrip/Managers/Loop/LoopManager.swift index 8352e14c9..6ec66f724 100644 --- a/xdrip/Managers/Loop/LoopManager.swift +++ b/xdrip/Managers/Loop/LoopManager.swift @@ -53,168 +53,190 @@ public class LoopManager:NSObject { // unwrap sharedUserDefaults guard let sharedUserDefaults = sharedUserDefaults else {return} - - trace("in share", log: log, category: ConstantsLog.categoryBlueToothTransmitter, type: .info) - - // get last readings with calculated value - // reduce timeStampLatestLoopSharedBgReading with 30 minutes. Because maybe Loop wasn't running for a while and so missed one or more readings. By adding 30 minutes of readings, we fill up a gap of maximum 30 minutes in Loop - let lastReadings = bgReadingsAccessor.getLatestBgReadings(limit: ConstantsShareWithLoop.maxReadingsToShareWithLoop, fromDate: UserDefaults.standard.timeStampLatestLoopSharedBgReading?.addingTimeInterval(-TimeInterval(minutes: 30)), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false) - - // calculate loopDelay, to avoid having to do it multiple times - let loopDelay = LoopManager.loopDelay() - - // if needed, remove readings less than loopDelay minutes old from glucoseData - if loopDelay > 0 { - - trace(" loopDelay = %{public}@. Deleting %{public}@ minutes of readings from glucoseData.",log: log, category: ConstantsLog.categoryLoopManager, type: .info, loopDelay.description) - - while glucoseData.count > 0 && glucoseData[0].timeStamp.addingTimeInterval(loopDelay) > Date() { - - glucoseData.remove(at: 0) - - } + + guard let timeStampLatestLoopSharedBgReading = UserDefaults.standard.timeStampLatestLoopSharedBgReading else { - // if no readings anymore, then no need to continue - if glucoseData.count == 0 { - return - } + // if the last share data hasn't been set previously (could only happen on the first run) then just set it and return until next bg reading is processed. We won't normally ever get to here + UserDefaults.standard.timeStampLatestLoopSharedBgReading = Date() - } else if lastReadings.count == 0 { - // this is the case where loopdelay = 0 and lastReadings is empty return + } - - // convert to json Dexcom Share format - var dictionary = [Dictionary]() + // to make things easier to read + let shareToLoopOnceEvery5Minutes = UserDefaults.standard.shareToLoopOnceEvery5Minutes - if loopDelay > 0 { - - for reading in glucoseData { + // if the user doesn't want to limit Loop Share OR (if they do AND more than 4.5 minutes has passed since the last time we shared data) then let's process the readings and share them + if !shareToLoopOnceEvery5Minutes || (shareToLoopOnceEvery5Minutes && Date().timeIntervalSince(timeStampLatestLoopSharedBgReading) > TimeInterval(minutes: 4.5)) { + + trace(" loopShare = Sharing data with Loop",log: log, category: ConstantsLog.categoryLoopManager, type: .info) + + // get last readings with calculated value + // reduce timeStampLatestLoopSharedBgReading with 30 minutes. Because maybe Loop wasn't running for a while and so missed one or more readings. By adding 30 minutes of readings, we fill up a gap of maximum 30 minutes in Loop + let lastReadings = bgReadingsAccessor.getLatestBgReadings(limit: ConstantsShareWithLoop.maxReadingsToShareWithLoop, fromDate: timeStampLatestLoopSharedBgReading.addingTimeInterval(-TimeInterval(minutes: 30)), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false) + + // calculate loopDelay, to avoid having to do it multiple times + let loopDelay = LoopManager.loopDelay() + + // if needed, remove readings less than loopDelay minutes old from glucoseData + if loopDelay > 0 { - var representation = reading.dictionaryRepresentationForLoopShare + trace(" loopDelay = %{public}@. Deleting %{public}@ minutes of readings from glucoseData.",log: log, category: ConstantsLog.categoryLoopManager, type: .info, loopDelay.description) - // Adding "from" field to be able to use multiple BG sources with the same shared group in FreeAPS X - representation["from"] = "xDrip" - dictionary.append(representation) - } - - } else { - - for reading in lastReadings { + while glucoseData.count > 0 && glucoseData[0].timeStamp.addingTimeInterval(loopDelay) > Date() { + + glucoseData.remove(at: 0) + + } - var representation = reading.dictionaryRepresentationForDexcomShareUpload + // if no readings anymore, then no need to continue + if glucoseData.count == 0 { + return + } - // Adding "from" field to be able to use multiple BG sources with the same shared group in FreeAPS X - representation["from"] = "xDrip" - dictionary.append(representation) + } else if lastReadings.count == 0 { + // this is the case where loopdelay = 0 and lastReadings is empty + return } - - } - - // now, if needed, increase the timestamp for each reading - if loopDelay > 0 { - // create new dictionary that will have the readings with timestamp increased - var newDictionary = [Dictionary]() - // iterate through dictionary - for reading in dictionary { + // convert to json Dexcom Share format + var dictionary = [Dictionary]() + + if loopDelay > 0 { - var readingTimeStamp: Date? - if let rawGlucoseStartDate = reading["DT"] as? String { - do { + for reading in glucoseData { + + var representation = reading.dictionaryRepresentationForLoopShare + + // Adding "from" field to be able to use multiple BG sources with the same shared group in FreeAPS X + representation["from"] = "xDrip" + dictionary.append(representation) + } + + } else { + + for reading in lastReadings { + + var representation = reading.dictionaryRepresentationForDexcomShareUpload + + // Adding "from" field to be able to use multiple BG sources with the same shared group in FreeAPS X + representation["from"] = "xDrip" + dictionary.append(representation) + } + + } + + // now, if needed, increase the timestamp for each reading + if loopDelay > 0 { + + // create new dictionary that will have the readings with timestamp increased + var newDictionary = [Dictionary]() + + // iterate through dictionary + for reading in dictionary { + + var readingTimeStamp: Date? + if let rawGlucoseStartDate = reading["DT"] as? String { + do { + + readingTimeStamp = try self.parseTimestamp(rawGlucoseStartDate) + + } catch { + + } + } + + if let readingTimeStamp = readingTimeStamp, let slopeOrdinal = reading["Trend"] as? Int, let value = reading["Value"] as? Double { - readingTimeStamp = try self.parseTimestamp(rawGlucoseStartDate) + // create new date : original date + loopDelay + let newReadingTimeStamp = readingTimeStamp.addingTimeInterval(loopDelay) - } catch { + // ignore the reading if newReadingTimeStamp > now + if newReadingTimeStamp < Date() { + + // this is for the json representation + let dateAsString = "/Date(" + Int64(floor(newReadingTimeStamp.toMillisecondsAsDouble() / 1000) * 1000).description + ")/" + + // create new reading and append to new dictionary + let newReading: [String : Any] = [ + "Trend" : slopeOrdinal, + "ST" : dateAsString, + "DT" : dateAsString, + "Value" : value, + "direction" : slopeOrdinal, + "from" : "xDrip" + ] + + newDictionary.append(newReading) + + } } + } - - if let readingTimeStamp = readingTimeStamp, let slopeOrdinal = reading["Trend"] as? Int, let value = reading["Value"] as? Double { - - // create new date : original date + loopDelay - let newReadingTimeStamp = readingTimeStamp.addingTimeInterval(loopDelay) - - // ignore the reading if newReadingTimeStamp > now - if newReadingTimeStamp < Date() { - - // this is for the json representation - let dateAsString = "/Date(" + Int64(floor(newReadingTimeStamp.toMillisecondsAsDouble() / 1000) * 1000).description + ")/" + + dictionary = newDictionary + + } + + // get Dictionary stored in UserDefaults from previous session + // append readings already stored in this storedDictionary so that we get dictionary filled with maxReadingsToShareWithLoop readings, if possible + if let storedDictionary = UserDefaults.standard.readingsStoredInSharedUserDefaultsAsDictionary, storedDictionary.count > 0 { + + let maxAmountsOfReadingsToAppend = ConstantsShareWithLoop.maxReadingsToShareWithLoop - dictionary.count + + if maxAmountsOfReadingsToAppend > 0 { + + let rangeToAppend = 0..<(min(storedDictionary.count, maxAmountsOfReadingsToAppend)) + + for value in storedDictionary[rangeToAppend] { + + dictionary.append(value) - // create new reading and append to new dictionary - let newReading: [String : Any] = [ - "Trend" : slopeOrdinal, - "ST" : dateAsString, - "DT" : dateAsString, - "Value" : value, - "direction" : slopeOrdinal, - "from" : "xDrip" - ] - - newDictionary.append(newReading) - } } } - - dictionary = newDictionary - } - - // get Dictionary stored in UserDefaults from previous session - // append readings already stored in this storedDictionary so that we get dictionary filled with maxReadingsToShareWithLoop readings, if possible - if let storedDictionary = UserDefaults.standard.readingsStoredInSharedUserDefaultsAsDictionary, storedDictionary.count > 0 { + guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { + return + } - let maxAmountsOfReadingsToAppend = ConstantsShareWithLoop.maxReadingsToShareWithLoop - dictionary.count + // write readings to shared user defaults + sharedUserDefaults.set(data, forKey: "latestReadings") - if maxAmountsOfReadingsToAppend > 0 { - - let rangeToAppend = 0..<(min(storedDictionary.count, maxAmountsOfReadingsToAppend)) + // store in local userdefaults + UserDefaults.standard.readingsStoredInSharedUserDefaultsAsDictionary = dictionary + + // initially set timeStampLatestLoopSharedBgReading to timestamp of first reading - may get another value later, in case loopdelay > 0 + // add 5 seconds to last Readings timestamp, because due to the way timestamp for libre readings is calculated, it may happen that the same reading shifts 1 or 2 seconds in next reading cycle + UserDefaults.standard.timeStampLatestLoopSharedBgReading = lastReadings.first!.timeStamp.addingTimeInterval(5.0) + + // in case loopdelay is used, then update UserDefaults.standard.timeStampLatestLoopSharedBgReading with value of timestamp of first element in the dictionary + if let element = dictionary.first, loopDelay > 0 { - for value in storedDictionary[rangeToAppend] { + if let elementDateAsString = element["DT"] as? String { - dictionary.append(value) + do { + if let readingTimeStamp = try self.parseTimestamp(elementDateAsString) { + UserDefaults.standard.timeStampLatestLoopSharedBgReading = readingTimeStamp + } + } catch { + // timeStampLatestLoopSharedBgReading keeps initially set value + } } } - } - - guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { - return - } - - // write readings to shared user defaults - sharedUserDefaults.set(data, forKey: "latestReadings") - - // store in local userdefaults - UserDefaults.standard.readingsStoredInSharedUserDefaultsAsDictionary = dictionary - - // initially set timeStampLatestLoopSharedBgReading to timestamp of first reading - may get another value later, in case loopdelay > 0 - // add 5 seconds to last Readings timestamp, because due to the way timestamp for libre readings is calculated, it may happen that the same reading shifts 1 or 2 seconds in next reading cycle - UserDefaults.standard.timeStampLatestLoopSharedBgReading = lastReadings.first!.timeStamp.addingTimeInterval(5.0) - - // in case loopdelay is used, then update UserDefaults.standard.timeStampLatestLoopSharedBgReading with value of timestamp of first element in the dictionary - if let element = dictionary.first, loopDelay > 0 { - - if let elementDateAsString = element["DT"] as? String { + } else { - do { - if let readingTimeStamp = try self.parseTimestamp(elementDateAsString) { - UserDefaults.standard.timeStampLatestLoopSharedBgReading = readingTimeStamp - } - } catch { - // timeStampLatestLoopSharedBgReading keeps initially set value - } - - } + trace(" loopDelay = Skipping Loop Share as user requests to limit sharing to 5 minutes and the last reading was <4.5 minutes ago at ",log: log, category: ConstantsLog.categoryLoopManager, type: .info, timeStampLatestLoopSharedBgReading.toStringInUserLocale(timeStyle: .short, dateStyle: .none, showTimeZone: false)) + } - + } /// calculate loop delay to use dependent on the time of the day, based on UserDefaults loopDelaySchedule and loopDelayValueInMinutes diff --git a/xdrip/Storyboards/en.lproj/SettingsViews.strings b/xdrip/Storyboards/en.lproj/SettingsViews.strings index 202ace5fe..28c2d0d9e 100644 --- a/xdrip/Storyboards/en.lproj/SettingsViews.strings +++ b/xdrip/Storyboards/en.lproj/SettingsViews.strings @@ -127,6 +127,7 @@ "settingsviews_housekeeperRetentionPeriodMessage" = "For how many days should data be stored? (Min 90, Max 365)\n\n(Recommended: 90 days)"; "suppressUnLockPayLoad" = "Suppress Unlock Payload"; "suppressLoopShare" = "Suppress Loop Share"; +"shareToLoopOnceEvery5Minutes" = "Share to Loop every 5 mins"; "Select Time" = "Select Time"; "Select Value" = "Select Value"; "expanatoryTextSelectTime" = "As of what time should the value apply"; diff --git a/xdrip/Texts/TextsSettingsView.swift b/xdrip/Texts/TextsSettingsView.swift index 32cef958f..c42641145 100644 --- a/xdrip/Texts/TextsSettingsView.swift +++ b/xdrip/Texts/TextsSettingsView.swift @@ -572,6 +572,10 @@ class Texts_SettingsView { static let warningLoopDelayAlreadyExists: String = { return NSLocalizedString("warningLoopDelayAlreadyExists", tableName: filename, bundle: Bundle.main, value: "There is already a loopDelay for this time.", comment: "When user creates new loopdelay, with a timestamp that already exists - this is the warning text") }() + + static let shareToLoopOnceEvery5Minutes: String = { + return NSLocalizedString("shareToLoopOnceEvery5Minutes", tableName: filename, bundle: Bundle.main, value: "Share to Loop every 5 mins", comment: "Should loop data be shared only every 5 minutes") + }() static let nsLog: String = { return NSLocalizedString("nslog", tableName: filename, bundle: Bundle.main, value: "NSLog", comment: "deloper settings, row title for NSLog - with NSLog enabled, a developer can view log information as explained here https://github.com/JohanDegraeve/xdripswift/wiki/NSLog") diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift index 1c92c1786..bebfef42d 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDevelopmentSettingsViewModel.swift @@ -17,10 +17,13 @@ fileprivate enum Setting:Int, CaseIterable { /// if true, then readings will not be written to shared user defaults (for loop) case suppressLoopShare = 4 + /// if true, then readings will only be written to shared user defaults (for loop) every 5 minutes (>4.5 mins to be exact) + case shareToLoopOnceEvery5Minutes = 5 + /// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values. /// /// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10) - case loopDelay = 5 + case loopDelay = 6 } @@ -59,6 +62,9 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { case .suppressLoopShare: return Texts_SettingsView.suppressLoopShare + case .shareToLoopOnceEvery5Minutes: + return Texts_SettingsView.shareToLoopOnceEvery5Minutes + case .loopDelay: return Texts_SettingsView.loopDelaysScreenTitle @@ -71,7 +77,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { switch setting { - case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare: + case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .shareToLoopOnceEvery5Minutes, .suppressLoopShare: return UITableViewCell.AccessoryType.none case .loopDelay: @@ -86,22 +92,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { switch setting { - case .NSLogEnabled: - return nil - - case .OSLogEnabled: - return nil - - case .smoothLibreValues: - return nil - - case .suppressUnLockPayLoad: - return nil - - case .suppressLoopShare: - return nil - - case .loopDelay: + case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare, .shareToLoopOnceEvery5Minutes, .loopDelay: return nil } @@ -154,6 +145,14 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { }) + case .shareToLoopOnceEvery5Minutes: + return UISwitch(isOn: UserDefaults.standard.shareToLoopOnceEvery5Minutes, action: { + (isOn:Bool) in + + UserDefaults.standard.shareToLoopOnceEvery5Minutes = isOn + + }) + case .loopDelay: return nil @@ -171,7 +170,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol { switch setting { - case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .suppressLoopShare: + case .NSLogEnabled, .OSLogEnabled, .smoothLibreValues, .suppressUnLockPayLoad, .shareToLoopOnceEvery5Minutes, .suppressLoopShare: return .nothing case .loopDelay: