Merge pull request #426 from paulplant/shared-data-frequency-limit

Add option to share to Loop just once every 5 minutes
This commit is contained in:
Johan Degraeve 2023-02-17 22:53:52 +01:00 committed by GitHub
commit 7300c7946d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 198 additions and 158 deletions

View File

@ -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

View File

@ -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 {
guard let timeStampLatestLoopSharedBgReading = UserDefaults.standard.timeStampLatestLoopSharedBgReading else {
trace(" loopDelay = %{public}@. Deleting %{public}@ minutes of readings from glucoseData.",log: log, category: ConstantsLog.categoryLoopManager, type: .info, loopDelay.description)
// 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()
while glucoseData.count > 0 && glucoseData[0].timeStamp.addingTimeInterval(loopDelay) > Date() {
glucoseData.remove(at: 0)
return
}
// to make things easier to read
let shareToLoopOnceEvery5Minutes = UserDefaults.standard.shareToLoopOnceEvery5Minutes
// 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 {
}
// if no readings anymore, then no need to continue
if glucoseData.count == 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)
}
// if no readings anymore, then no need to continue
if glucoseData.count == 0 {
return
}
} else if lastReadings.count == 0 {
// this is the case where loopdelay = 0 and lastReadings is empty
return
}
} 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<String, Any>]()
if loopDelay > 0 {
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<String, Any>]()
// convert to json Dexcom Share format
var dictionary = [Dictionary<String, Any>]()
// iterate through dictionary
for reading in dictionary {
if loopDelay > 0 {
var readingTimeStamp: Date?
if let rawGlucoseStartDate = reading["DT"] as? String {
do {
readingTimeStamp = try self.parseTimestamp(rawGlucoseStartDate)
} catch {
}
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)
}
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 + ")/"
} 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<String, Any>]()
// 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 {
// create new reading and append to new dictionary
let newReading: [String : Any] = [
"Trend" : slopeOrdinal,
"ST" : dateAsString,
"DT" : dateAsString,
"Value" : value,
"direction" : slopeOrdinal,
"from" : "xDrip"
// 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 + ")/"
// 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)
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 {
let maxAmountsOfReadingsToAppend = ConstantsShareWithLoop.maxReadingsToShareWithLoop - dictionary.count
if maxAmountsOfReadingsToAppend > 0 {
let rangeToAppend = 0..<(min(storedDictionary.count, maxAmountsOfReadingsToAppend))
for value in storedDictionary[rangeToAppend] {
dictionary.append(value)
}
}
}
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)
}
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else {
return
}
}
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 {
// 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 {
do {
if let readingTimeStamp = try self.parseTimestamp(elementDateAsString) {
UserDefaults.standard.timeStampLatestLoopSharedBgReading = readingTimeStamp
if let elementDateAsString = element["DT"] as? String {
do {
if let readingTimeStamp = try self.parseTimestamp(elementDateAsString) {
UserDefaults.standard.timeStampLatestLoopSharedBgReading = readingTimeStamp
}
} catch {
// timeStampLatestLoopSharedBgReading keeps initially set value
}
} catch {
// timeStampLatestLoopSharedBgReading keeps initially set value
}
}
} else {
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

View File

@ -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";

View File

@ -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")

View File

@ -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: