Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions LoopFollow/Controllers/Nightscout/DeviceStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -182,10 +182,10 @@ 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,
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)
Expand Down Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 22 additions & 9 deletions Tests/DeviceStatusHistoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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"},
Expand Down Expand Up @@ -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}}
Expand All @@ -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,
Expand All @@ -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]}}}
""")
Expand All @@ -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")
Expand Down