Skip to content

Commit 89c22ff

Browse files
authored
Fix missing Trio FPU entries (#741)
LoopFollow was deduplicating Nightscout treatments by Trio's shared FPU ID, which hid the first scheduled entry from the graph. Deduplicate by event type and occurrence time as well, preserving distinct FPU entries while still collapsing true duplicates.
1 parent 835689a commit 89c22ff

2 files changed

Lines changed: 98 additions & 6 deletions

File tree

LoopFollow/Controllers/Nightscout/Treatments.swift

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,39 @@
44
import Foundation
55

66
extension MainViewController {
7+
private struct TreatmentOccurrence: Hashable {
8+
let id: String
9+
let eventType: String?
10+
let date: Date?
11+
}
12+
13+
/// Nightscout duplicates can have different MongoDB `_id` values. Trio FPU siblings share an `id` but have distinct
14+
/// times, so deduplicate by logical occurrence rather than `id`.
15+
static func deduplicatedTreatmentEntries(_ entries: [[String: AnyObject]]) -> [[String: AnyObject]] {
16+
var seenOccurrences = Set<TreatmentOccurrence>()
17+
return entries.filter { entry in
18+
guard let id = entry["id"] as? String, !id.isEmpty else { return true }
19+
let eventType = entry["eventType"] as? String
20+
let occurrence = TreatmentOccurrence(
21+
id: id,
22+
eventType: eventType,
23+
date: treatmentOccurrenceDate(entry, eventType: eventType)
24+
)
25+
return seenOccurrences.insert(occurrence).inserted
26+
}
27+
}
28+
29+
private static func treatmentOccurrenceDate(_ entry: [String: AnyObject], eventType: String?) -> Date? {
30+
let rawDate: String?
31+
switch eventType {
32+
case "Pump Site Change", "Site Change", "Sensor Start", "Insulin Change":
33+
rawDate = entry["created_at"] as? String
34+
default:
35+
rawDate = (entry["timestamp"] as? String) ?? (entry["created_at"] as? String)
36+
}
37+
return rawDate.flatMap(NightscoutUtils.parseDate)
38+
}
39+
740
// NS Treatments Web Call
841
// Downloads Basal, Bolus, Carbs, BG Check, Notes, Overrides
942
func WebLoadNSTreatments() {
@@ -35,12 +68,7 @@ extension MainViewController {
3568

3669
// Process and split out treatments to individual tasks
3770
func updateTreatments(entries: [[String: AnyObject]]) {
38-
// Deduplicate entries by "id" field (Trio/Loop UUID)
39-
var seenIDs = Set<String>()
40-
let uniqueEntries = entries.filter { entry in
41-
guard let id = entry["id"] as? String else { return true }
42-
return seenIDs.insert(id).inserted
43-
}
71+
let uniqueEntries = Self.deduplicatedTreatmentEntries(entries)
4472

4573
var tempBasal: [[String: AnyObject]] = []
4674
var bolus: [[String: AnyObject]] = []
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// LoopFollow
2+
// NightscoutTreatmentDeduplicationTests.swift
3+
4+
import Foundation
5+
@testable import LoopFollow
6+
import Testing
7+
8+
struct NightscoutTreatmentDeduplicationTests {
9+
private typealias Entry = [String: AnyObject]
10+
11+
@Test("keeps Trio FPU siblings with one id and distinct times")
12+
func keepsFPUOccurrences() {
13+
let entries = [
14+
entry("newer", createdAt: "2026-08-16T02:06:00Z"),
15+
entry("older", createdAt: "2026-08-16T01:06:00Z"),
16+
]
17+
#expect(deduplicatedIDs(entries) == ["newer", "older"])
18+
}
19+
20+
@Test("collapses duplicate Nightscout documents and keeps the first")
21+
func collapsesDuplicates() {
22+
let entries = [
23+
entry("first", createdAt: "2026-08-16T01:06:00Z"),
24+
entry("duplicate", createdAt: "2026-08-16T01:06:00Z"),
25+
]
26+
#expect(deduplicatedIDs(entries) == ["first"])
27+
}
28+
29+
@Test("uses normalized effective time and event type")
30+
func usesLogicalOccurrence() {
31+
let first = entry("first", timestamp: "2026-08-16T01:06:00Z", createdAt: "2026-08-16T01:05:58Z")
32+
let equivalent = entry("equivalent", timestamp: "2026-08-16T01:06:00.000Z", createdAt: "2026-08-16T01:06:02Z")
33+
let createdAtOnly = entry("created-at", timestamp: nil, createdAt: "2026-08-16T01:06:00Z")
34+
let bolus = entry("bolus", eventType: "Correction Bolus", timestamp: "2026-08-16T01:06:00Z")
35+
#expect(deduplicatedIDs([first, equivalent, createdAtOnly, bolus]) == ["first", "bolus"])
36+
}
37+
38+
@Test("preserves missing identifiers and fallback behavior")
39+
func preservesFallbacks() {
40+
let noID = entry("no-id", id: nil)
41+
let blankID = entry("blank-id", id: "")
42+
let noTime = [entry("no-time"), entry("no-time-duplicate")]
43+
let sensor = entry("sensor", eventType: "Sensor Start", timestamp: "2026-08-16T02:00:00Z", createdAt: "2026-08-16T01:00:00Z")
44+
let sensorDuplicate = entry("sensor-duplicate", eventType: "Sensor Start", timestamp: "2026-08-16T03:00:00Z", createdAt: "2026-08-16T01:00:00Z")
45+
#expect(deduplicatedIDs([noID, noID, blankID, blankID]) == ["no-id", "no-id", "blank-id", "blank-id"])
46+
#expect(deduplicatedIDs(noTime) == ["no-time"])
47+
#expect(deduplicatedIDs([sensor, sensorDuplicate]) == ["sensor"])
48+
}
49+
50+
private func deduplicatedIDs(_ entries: [Entry]) -> [String] {
51+
MainViewController.deduplicatedTreatmentEntries(entries).compactMap { $0["_id"] as? String }
52+
}
53+
54+
private func entry(_ mongoID: String, id: String? = "shared-id", eventType: String = "Carb Correction", timestamp: String? = nil, createdAt: String? = nil) -> Entry {
55+
var result: Entry = [
56+
"_id": mongoID as AnyObject,
57+
"eventType": eventType as AnyObject,
58+
]
59+
if let id { result["id"] = id as AnyObject }
60+
if let timestamp { result["timestamp"] = timestamp as AnyObject }
61+
if let createdAt { result["created_at"] = createdAt as AnyObject }
62+
return result
63+
}
64+
}

0 commit comments

Comments
 (0)