Skip to content

Commit

Permalink
Merge pull request #426 from paulplant/shared-data-frequency-limit
Browse files Browse the repository at this point in the history
Add option to share to Loop just once every 5 minutes
  • Loading branch information
JohanDegraeve authored Feb 17, 2023
2 parents 2342096 + cb015ce commit 7300c79
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 146 deletions.
18 changes: 16 additions & 2 deletions xdrip/Extensions/UserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
272 changes: 147 additions & 125 deletions xdrip/Managers/Loop/LoopManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any>]()
// 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<String, Any>]()

// iterate through dictionary
for reading in dictionary {
// convert to json Dexcom Share format
var dictionary = [Dictionary<String, Any>]()

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

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
Expand Down
1 change: 1 addition & 0 deletions xdrip/Storyboards/en.lproj/SettingsViews.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 4 additions & 0 deletions xdrip/Texts/TextsSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading

0 comments on commit 7300c79

Please sign in to comment.