From 8a7294519b56145d34b58f5d49c0c714310bb10a Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 27 Oct 2026 13:07:19 -0700 Subject: [PATCH 1/2] Modifications from PR #794 --- LoopFollow/Controllers/Nightscout/DeviceStatus.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index a4741037b..08118c3d7 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -92,7 +92,7 @@ extension MainViewController { Observable.shared.previousAlertLastLoopTime.value = Observable.shared.alertLastLoopTime.value - if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? { + if let lastPumpRecord = lastDeviceStatus?["pump"] as? [String: AnyObject] { if let bolusIncrement = lastPumpRecord["bolusIncrement"] as? Double, bolusIncrement > 0 { Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: bolusIncrement) Storage.shared.bolusIncrementDetected.value = true @@ -182,7 +182,7 @@ extension MainViewController { } // Loop - handle new data - if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? { + if let lastLoopRecord = lastDeviceStatus?["loop"] as? [String: AnyObject] { // Some pumps report no `pump.clock`; without it alertLastLoopTime stays 0 // and the forecast anchors to epoch 0. Fall back to the loop cycle timestamp. if (lastDeviceStatus?["pump"] as? [String: AnyObject])?["clock"] == nil, @@ -223,7 +223,7 @@ extension MainViewController { } // OpenAPS - handle new data - if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String: AnyObject]? { + if let lastLoopRecord = lastDeviceStatus?["openaps"] as? [String: AnyObject] { DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord) } From 64daf741b0bbe4c5e24badf653fe09d8145d501d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 Oct 2026 19:45:22 +0100 Subject: [PATCH 2/2] Treat explicit-null failureReason and pump clock as missing, drop force cast in test helper --- .../Controllers/Nightscout/DeviceStatus.swift | 2 +- .../Nightscout/DeviceStatusLoop.swift | 2 +- .../Nightscout/DeviceStatusOpenAPS.swift | 2 +- Tests/DeviceStatusHistoryTests.swift | 31 +++++++++++++------ 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 08118c3d7..87c44f676 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -185,7 +185,7 @@ extension MainViewController { if let lastLoopRecord = lastDeviceStatus?["loop"] as? [String: AnyObject] { // Some pumps report no `pump.clock`; without it alertLastLoopTime stays 0 // and the forecast anchors to epoch 0. Fall back to the loop cycle timestamp. - if (lastDeviceStatus?["pump"] as? [String: AnyObject])?["clock"] == nil, + if (lastDeviceStatus?["pump"] as? [String: AnyObject])?["clock"] as? String == nil, let loopTimestampString = lastLoopRecord["timestamp"] as? String, let loopTimestamp = formatter.date(from: loopTimestampString)?.timeIntervalSince1970, loopTimestamp > (Observable.shared.alertLastLoopTime.value ?? 0) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 8414b2457..1727c0729 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -32,7 +32,7 @@ extension MainViewController { Storage.shared.lastIOB.value = latestIOB?.value Storage.shared.lastCOB.value = latestCOB?.value - if lastLoopRecord["failureReason"] != nil { + if let failureReason = lastLoopRecord["failureReason"], !(failureReason is NSNull) { Observable.shared.loopStatusText.value = "X" latestLoopStatusString = "X" } else { diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index e9cb08ba5..ceaef95c5 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -7,7 +7,7 @@ import HealthKit extension MainViewController { func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { Storage.shared.device.value = lastDeviceStatus?["device"] as? String ?? "" - if lastLoopRecord["failureReason"] != nil { + if let failureReason = lastLoopRecord["failureReason"], !(failureReason is NSNull) { Observable.shared.loopStatusText.value = "X" latestLoopStatusString = "X" } else { diff --git a/Tests/DeviceStatusHistoryTests.swift b/Tests/DeviceStatusHistoryTests.swift index b2b9f87f2..39d3f1770 100644 --- a/Tests/DeviceStatusHistoryTests.swift +++ b/Tests/DeviceStatusHistoryTests.swift @@ -6,13 +6,13 @@ import Foundation import Testing struct DeviceStatusHistoryParserTests { - private func json(_ text: String) -> [String: Any] { - try! JSONSerialization.jsonObject(with: Data(text.utf8)) as! [String: Any] + private func json(_ text: String) throws -> [String: Any] { + try #require(JSONSerialization.jsonObject(with: Data(text.utf8)) as? [String: Any]) } @Test("Loop record yields IOB, COB and the forecast from its start date") func loopRecord() throws { - let record = json(""" + let record = try json(""" {"created_at": "2026-09-27T10:00:30.000Z", "loop": {"timestamp": "2026-09-27T10:00:05Z", "iob": {"iob": 1.25, "timestamp": "2026-09-27T10:00:00Z"}, @@ -32,7 +32,7 @@ struct DeviceStatusHistoryParserTests { @Test("Trio record yields smoothed BG, ISF, ratio and the four forecast curves") func trioRecord() throws { - let record = json(""" + let record = try json(""" {"created_at": "2026-09-27T10:00:30.000Z", "device": "Trio", "pump": {"reservoir": 120.5, "battery": {"percent": 80}}, "uploader": {"battery": 55}, "openaps": {"iob": {"iob": 2.5, "time": "2026-09-27T10:00:00Z"}, @@ -61,7 +61,7 @@ struct DeviceStatusHistoryParserTests { @Test("Loop's active override is kept") func loopOverride() throws { - let record = json(""" + let record = try json(""" {"created_at": "2026-09-27T10:00:30Z", "override": {"active": true, "multiplier": 1.5, "currentCorrectionRange": {"minValue": 90, "maxValue": 100}}, "loop": {"timestamp": "2026-09-27T10:00:05Z", "recommendedBolus": 0.4}} @@ -73,7 +73,7 @@ struct DeviceStatusHistoryParserTests { @Test("A zero ISF is a placeholder, COB falls back to the reason string") func openAPSFallbacks() throws { - let record = json(""" + let record = try json(""" {"created_at": "2026-09-27T10:00:30Z", "openaps": {"iob": [{"iob": 0.8}, {"iob": 0.6}], "enacted": {"timestamp": "2026-09-27T10:00:00Z", "ISF": 0, @@ -87,7 +87,7 @@ struct DeviceStatusHistoryParserTests { @Test("Out-of-range forecast values are clamped") func hugeForecastValue() throws { - let record = json(""" + let record = try json(""" {"created_at": "2026-09-27T10:00:30Z", "loop": {"timestamp": "2026-09-27T10:00:05Z", "predicted": {"values": [1e30, -5, 120]}}} """) @@ -96,13 +96,26 @@ struct DeviceStatusHistoryParserTests { } @Test("Uploader-only records carry nothing to graph") - func uploaderOnly() { - let record = json(""" + func uploaderOnly() throws { + let record = try json(""" {"created_at": "2026-09-27T10:00:30Z", "uploader": {"battery": 80}} """) #expect(DeviceStatusHistoryParser.parse(record) == nil) } + @Test("Explicit nulls are treated as missing fields") + func explicitNulls() throws { + let record = try json(""" + {"created_at": "2026-09-27T10:00:30Z", "loop": null, "override": null, "pump": {"clock": null}, + "openaps": {"iob": [{"iob": 0.8}], "suggested": null, + "enacted": {"timestamp": "2026-09-27T10:00:00Z", "COB": 12, "ISF": null}}} + """) + let (sample, _) = try #require(DeviceStatusHistoryParser.parse(record)) + #expect(sample.iob == 0.8) + #expect(sample.cob == 12) + #expect(sample.isf == nil) + } + @Test("Offsets are honored") func offsetDates() { let utc = DeviceStatusHistoryParser.parseDate("2026-09-27T10:00:00Z")