diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 168139ef..a15535ec 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -9,6 +9,16 @@ /* Begin PBXBuildFile section */ 3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; }; DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; }; + DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */; }; + DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C612C4175FD00DBADDF /* NSProfile.swift */; }; + DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */; }; + DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */; }; + DD0C0C682C48529400DBADDF /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C672C48529400DBADDF /* Metric.swift */; }; + DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */; }; + DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */; }; + DD13BC752C3FD6210062313B /* InfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC742C3FD6200062313B /* InfoType.swift */; }; + DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC762C3FD64E0062313B /* InfoData.swift */; }; + DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC782C3FE63A0062313B /* InfoManager.swift */; }; DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */; }; DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD42ACF2109009A6922 /* ResumePump.swift */; }; DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AD62ACF2139009A6922 /* SuspendPump.swift */; }; @@ -22,8 +32,8 @@ DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; }; DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; }; DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */; }; - DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; }; DD608A0A2C23593900F91132 /* SMB.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A092C23593900F91132 /* SMB.swift */; }; + DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; }; DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */; }; DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19832ACDA50C00DBD158 /* Overrides.swift */; }; DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; }; @@ -193,7 +203,7 @@ FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD2A27C24C9D044009F7B7B /* Globals.swift */; }; FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD49B6B24AA536E007879DC /* DebugViewController.swift */; }; FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */; }; - FCEF87AC24A141A700AE6FA0 /* bgUnits.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEF87AA24A1417900AE6FA0 /* bgUnits.swift */; }; + FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEF87AA24A1417900AE6FA0 /* Localizer.swift */; }; FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; }; FCFEECA02488157B00402A7F /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEEC9F2488157B00402A7F /* Chart.swift */; }; FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; }; @@ -203,6 +213,16 @@ 059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = ""; }; A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; + DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; + DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; + DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKUnit+Extensions.swift"; sourceTree = ""; }; + DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDataSeparator.swift; sourceTree = ""; }; + DD0C0C672C48529400DBADDF /* Metric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metric.swift; sourceTree = ""; }; + DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinMetric.swift; sourceTree = ""; }; + DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbMetric.swift; sourceTree = ""; }; + DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = ""; }; + DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = ""; }; + DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = ""; }; DD152D3A24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDisplaySettingsViewController.swift; sourceTree = ""; }; DD493AD42ACF2109009A6922 /* ResumePump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumePump.swift; sourceTree = ""; }; DD493AD62ACF2139009A6922 /* SuspendPump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendPump.swift; sourceTree = ""; }; @@ -216,8 +236,8 @@ DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = ""; }; DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = ""; }; DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusLoop.swift; sourceTree = ""; }; - DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = ""; }; DD608A092C23593900F91132 /* SMB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMB.swift; sourceTree = ""; }; + DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = ""; }; DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusOpenAPS.swift; sourceTree = ""; }; DD7E19832ACDA50C00DBD158 /* Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overrides.swift; sourceTree = ""; }; DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = ""; }; @@ -393,7 +413,7 @@ FCD49B6B24AA536E007879DC /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = ""; }; FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbBolusArrays.swift; sourceTree = ""; }; FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; - FCEF87AA24A1417900AE6FA0 /* bgUnits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = bgUnits.swift; sourceTree = ""; }; + FCEF87AA24A1417900AE6FA0 /* Localizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizer.swift; sourceTree = ""; }; FCFEEC9D2486E68E00402A7F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; FCFEEC9F2488157B00402A7F /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = ""; }; FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; @@ -431,6 +451,27 @@ path = Pods; sourceTree = ""; }; + DD0C0C692C4852A100DBADDF /* Metric */ = { + isa = PBXGroup; + children = ( + DD0C0C672C48529400DBADDF /* Metric.swift */, + DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */, + DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */, + ); + path = Metric; + sourceTree = ""; + }; + DD13BC732C3FD60E0062313B /* InfoTable */ = { + isa = PBXGroup; + children = ( + DD13BC742C3FD6200062313B /* InfoType.swift */, + DD13BC762C3FD64E0062313B /* InfoData.swift */, + DD13BC782C3FE63A0062313B /* InfoManager.swift */, + DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */, + ); + path = InfoTable; + sourceTree = ""; + }; DD493AEA2ACF2761009A6922 /* Treatments */ = { isa = PBXGroup; children = ( @@ -461,6 +502,8 @@ DD493AE82ACF2445009A6922 /* BGData.swift */, DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */, DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */, + DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */, + DD0C0C612C4175FD00DBADDF /* NSProfile.swift */, ); path = Nightscout; sourceTree = ""; @@ -471,6 +514,7 @@ DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */, DDCF979324C0D380002C9752 /* UIViewExtension.swift */, DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */, + DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -646,6 +690,8 @@ FC8DEEE32485D1680075863F /* LoopFollow */ = { isa = PBXGroup; children = ( + DD0C0C692C4852A100DBADDF /* Metric */, + DD13BC732C3FD60E0062313B /* InfoTable */, FCC688702489A57C00A0279D /* Loop Follow.entitlements */, FC8DEEE62485D1ED0075863F /* Info.plist */, FC7CE59A248D334B001F83B8 /* Resources */, @@ -704,7 +750,7 @@ FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */, FC16A98024996C07003D6245 /* DateTime.swift */, FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */, - FCEF87AA24A1417900AE6FA0 /* bgUnits.swift */, + FCEF87AA24A1417900AE6FA0 /* Localizer.swift */, FC1BDD2E24A232A3001B652C /* DataStructs.swift */, FCD2A27C24C9D044009F7B7B /* Globals.swift */, FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */, @@ -1009,12 +1055,15 @@ FC16A97D24996747003D6245 /* Alarms.swift in Sources */, FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */, DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */, + DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */, DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, + DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */, FCFEECA02488157B00402A7F /* Chart.swift in Sources */, DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */, + DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */, DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */, - FCEF87AC24A141A700AE6FA0 /* bgUnits.swift in Sources */, + FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */, FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */, FCC6886524898EEE00A0279D /* UserDefaults.swift in Sources */, FC16A97F249969E2003D6245 /* Graphs.swift in Sources */, @@ -1024,6 +1073,8 @@ DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */, DD152D3B24C01B2300361FA2 /* InfoDisplaySettingsViewController.swift in Sources */, DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */, + DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */, + DD13BC752C3FD6210062313B /* InfoType.swift in Sources */, DDCF979C24C14EFB002C9752 /* AdvancedSettingsViewController.swift in Sources */, FC97881C2485969B00A7906C /* MainViewController.swift in Sources */, DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */, @@ -1033,20 +1084,25 @@ DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */, FC9788182485969B00A7906C /* AppDelegate.swift in Sources */, DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */, + DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */, DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */, DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */, DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */, + DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */, DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */, + DD0C0C682C48529400DBADDF /* Metric.swift in Sources */, FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */, FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */, + DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */, FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */, FC1BDD2B24A22650001B652C /* Stats.swift in Sources */, FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */, FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */, FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */, + DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */, DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */, DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */, FCA2DDE62501095000254A8C /* Timers.swift in Sources */, diff --git a/LoopFollow/Controllers/Alarms.swift b/LoopFollow/Controllers/Alarms.swift index d710c3b5..b06f7abc 100644 --- a/LoopFollow/Controllers/Alarms.swift +++ b/LoopFollow/Controllers/Alarms.swift @@ -86,7 +86,7 @@ extension MainViewController { } if bolusCount >= UserDefaultsRepository.alertIOBNumber.value || totalBoluses >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) || - Double(latestIOB) ?? 0 >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) { + (latestIOB?.value ?? 0) >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) { AlarmSound.whichAlarm = "IOB Alert" //determine if it is day or night and what should happen if UserDefaultsRepository.nightTime.value { @@ -104,7 +104,7 @@ extension MainViewController { // Check COB if UserDefaultsRepository.alertCOB.value && !UserDefaultsRepository.alertCOBIsSnoozed.value { let alertAt = Double(UserDefaultsRepository.alertCOBAt.value) - if Double(latestCOB) ?? 0 >= alertAt { + if (latestCOB?.value ?? 0) >= alertAt { AlarmSound.whichAlarm = "COB Alert" //determine if it is day or night and what should happen if UserDefaultsRepository.nightTime.value { @@ -586,7 +586,7 @@ extension MainViewController { if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: bgUnits.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: Localizer.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) if audio && !UserDefaultsRepository.alertMuteAllIsMuted.value && audioDuringCall{ AlarmSound.setSoundFile(str: sound) AlarmSound.play(overrideVolume: overrideVolume, numLoops: numLoops) @@ -600,7 +600,7 @@ extension MainViewController { if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: bgUnits.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: Localizer.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) snoozer.SnoozeButton.isHidden = false snoozer.AlertLabel.isHidden = false snoozer.clockLabel.isHidden = true @@ -633,7 +633,7 @@ extension MainViewController { AlarmSound.whichAlarm = "none" guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: bgUnits.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: Localizer.toDisplayUnits(latestDeltaString) ?? "", minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) snoozer.SnoozeButton.isHidden = true snoozer.AlertLabel.isHidden = true if AlarmSound.isPlaying { @@ -1026,14 +1026,14 @@ extension MainViewController { let texts = AnnouncementTexts.forLanguage(preferredLanguage) let negligibleThreshold = 3 - let localizedCurrentValue = bgUnits.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".") + let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".") let announcementText: String if abs(bloodGlucoseDifference) <= negligibleThreshold { announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)" } else { let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase - let absoluteDifference = bgUnits.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".") + let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".") announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)" } diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift index 468ca28a..771c42cd 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -636,7 +636,7 @@ extension MainViewController { if Float(entries[i].sgv) > topBG - maxBGOffset { topBG = Float(entries[i].sgv) + maxBGOffset } - let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].sgv), data: formatPillText(line1: bgUnits.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date)) + let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date)) if UserDefaultsRepository.debugLog.value { writeDebugLog(value: "BG: " + value.description) } mainChart.append(value) smallChart.append(value) @@ -731,7 +731,7 @@ extension MainViewController { colors.append(color ?? NSUIColor.systemPurple) } - let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: bgUnits.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date)) + let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: Localizer.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date)) mainChart.addEntry(value) smallChart.addEntry(value) } @@ -1025,7 +1025,7 @@ extension MainViewController { let graphHours = 24 * UserDefaultsRepository.downloadDays.value if bgCheckData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - let value = ChartDataEntry(x: Double(bgCheckData[i].date), y: Double(bgCheckData[i].sgv), data: formatPillText(line1: bgUnits.toDisplayUnits(String(bgCheckData[i].sgv)), time: bgCheckData[i].date)) + let value = ChartDataEntry(x: Double(bgCheckData[i].date), y: Double(bgCheckData[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(bgCheckData[i].sgv)), time: bgCheckData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) if UserDefaultsRepository.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) @@ -1573,7 +1573,7 @@ extension MainViewController { data: formatPillText( line1: chartLabel, time: predictionData[i].date, - line2: bgUnits.toDisplayUnits(String(predictionVal)) + line2: Localizer.toDisplayUnits(String(predictionVal)) ) ) mainChart.addEntry(value) diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index e765dc44..49caad41 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -19,29 +19,13 @@ extension MainViewController { var created_at: String } - //NS Basal Profile Struct struct basalProfileStruct: Codable { var value: Double var time: String var timeAsSeconds: Double } - - struct NSProfile: Decodable { - struct Store: Decodable { - struct BasalEntry: Decodable { - let value: Double - let time: String - let timeAsSeconds: Double - } - - let basal: [BasalEntry] - } - - let store: [String: Store] - let defaultProfile: String - } - + //NS Basal Data Struct struct basalGraphStruct: Codable { var basalRate: Double diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index 3f509ecd..3bdc6692 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -239,8 +239,8 @@ extension MainViewController { var snoozerDelta = "" // Set BGText with the latest BG value - self.BGText.text = bgUnits.toDisplayUnits(String(latestBG)) - snoozerBG = bgUnits.toDisplayUnits(String(latestBG)) + self.BGText.text = Localizer.toDisplayUnits(String(latestBG)) + snoozerBG = Localizer.toDisplayUnits(String(latestBG)) self.setBGTextColor() // Direction handling @@ -256,12 +256,12 @@ extension MainViewController { // Delta handling if deltaBG < 0 { - self.DeltaText.text = bgUnits.toDisplayUnits(String(deltaBG)) - snoozerDelta = bgUnits.toDisplayUnits(String(deltaBG)) + self.DeltaText.text = Localizer.toDisplayUnits(String(deltaBG)) + snoozerDelta = Localizer.toDisplayUnits(String(deltaBG)) self.latestDeltaString = String(deltaBG) } else { - self.DeltaText.text = "+" + bgUnits.toDisplayUnits(String(deltaBG)) - snoozerDelta = "+" + bgUnits.toDisplayUnits(String(deltaBG)) + self.DeltaText.text = "+" + Localizer.toDisplayUnits(String(deltaBG)) + snoozerDelta = "+" + Localizer.toDisplayUnits(String(deltaBG)) self.latestDeltaString = "+" + String(deltaBG) } diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index c56abf03..0a714400 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -30,7 +30,7 @@ extension MainViewController { // NS Cage Response Processor func updateCage(data: [cageData]) { - self.clearLastInfoData(index: 7) + infoManager.clearInfoData(type: .cage) if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: CAGE") } if data.count == 0 { return @@ -55,9 +55,9 @@ extension MainViewController { formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale - let formattedDuration = formatter.string(from: secondsAgo) - tableData[7].value = formattedDuration ?? "" + if let formattedDuration = formatter.string(from: secondsAgo) { + infoManager.updateInfoData(type: .cage, value: formattedDuration) + } } - infoTable.reloadData() } } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 0d2b894d..170b0d65 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -83,11 +83,8 @@ extension MainViewController { // NS Device Status Response Processor func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) { - self.clearLastInfoData(index: 0) - self.clearLastInfoData(index: 1) - self.clearLastInfoData(index: 3) - self.clearLastInfoData(index: 4) - self.clearLastInfoData(index: 5) + infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio, .updated, .recBolus, .tdd]) + if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") } if jsonDeviceStatus.count == 0 { return @@ -106,15 +103,15 @@ extension MainViewController { if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 { if let reservoirData = lastPumpRecord["reservoir"] as? Double { latestPumpVolume = reservoirData - tableData[5].value = String(format:"%.0f", reservoirData) + "U" + infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U") } else { latestPumpVolume = 50.0 - tableData[5].value = "50+U" + infoManager.updateInfoData(type: .pump, value: "50+U") } - - if let uploader = lastDeviceStatus?["uploader"] as? [String:AnyObject] { - let upbat = uploader["battery"] as! Double - tableData[4].value = String(format:"%.0f", upbat) + "%" + + if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject], + let upbat = uploader["battery"] as? Double { + infoManager.updateInfoData(type: .battery, value: String(format: "%.0f", upbat) + "%") UserDefaultsRepository.deviceBatteryLevel.value = upbat } } @@ -130,31 +127,30 @@ extension MainViewController { DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord) } - var oText = "" as String + var oText = "" currentOverride = 1.0 - if let lastOverride = lastDeviceStatus?["override"] as! [String : AnyObject]? { - if lastOverride["active"] as! Bool { + if let lastOverride = lastDeviceStatus?["override"] as? [String: AnyObject], + let isActive = lastOverride["active"] as? Bool, isActive { + if let lastCorrection = lastOverride["currentCorrectionRange"] as? [String: AnyObject], + let minValue = lastCorrection["minValue"] as? Double, + let maxValue = lastCorrection["maxValue"] as? Double { - let lastCorrection = lastOverride["currentCorrectionRange"] as! [String: AnyObject] if let multiplier = lastOverride["multiplier"] as? Double { currentOverride = multiplier oText += String(format: "%.0f%%", (multiplier * 100)) - } - else - { + } else { oText += "100%" } - oText += " (" - let minValue = lastCorrection["minValue"] as! Double - let maxValue = lastCorrection["maxValue"] as! Double - oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")" - tableData[3].value = oText + oText += " (" + oText += Localizer.toDisplayUnits(String(minValue)) + "-" + Localizer.toDisplayUnits(String(maxValue)) + ")" } + + infoManager.updateInfoData(type: .override, value: oText) + } else { + infoManager.clearInfoData(type: .override) } - - infoTable.reloadData() - + // Start the timer based on the timestamp let now = dateTimeUtils.getNowTimeIntervalUTC() let secondsAgo = now - latestLoopTime diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 868cd1a6..2ca0ee25 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -9,6 +9,7 @@ import Foundation import UIKit import Charts +import HealthKit extension MainViewController { func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) { @@ -29,17 +30,44 @@ extension MainViewController { } } - if let iobdata = lastLoopRecord["iob"] as? [String:AnyObject] { - tableData[0].value = String(format:"%.2f", (iobdata["iob"] as! Double)) - latestIOB = String(format:"%.2f", (iobdata["iob"] as! Double)) + + // ISF + let profileISF = profileManager.currentISF() + if let profileISF = profileISF { + infoManager.updateInfoData(type: .isf, value: profileISF) + } + + // Carb Ratio (CR) + let profileCR = profileManager.currentCarbRatio() + if let profileCR = profileCR { + infoManager.updateInfoData(type: .carbRatio, value: profileCR) } - if let cobdata = lastLoopRecord["cob"] as? [String:AnyObject] { - tableData[1].value = String(format:"%.0f", cobdata["cob"] as! Double) - latestCOB = String(format:"%.0f", cobdata["cob"] as! Double) + + // Target + let profileTargetLow = profileManager.currentTargetLow() + let profileTargetHigh = profileManager.currentTargetHigh() + + if let profileTargetLow = profileTargetLow, let profileTargetHigh = profileTargetHigh, profileTargetLow != profileTargetHigh { + infoManager.updateInfoData(type: .target, firstValue: profileTargetLow, secondValue: profileTargetHigh, separator: .dash) + } else if let profileTargetLow = profileTargetLow { + infoManager.updateInfoData(type: .target, value: profileTargetLow) + } + + // IOB + if let insulinMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") { + infoManager.updateInfoData(type: .iob, value: insulinMetric) + latestIOB = insulinMetric } + + // COB + if let cobMetric = CarbMetric(from: lastLoopRecord["cob"], key: "cob") { + infoManager.updateInfoData(type: .cob, value: cobMetric) + latestCOB = cobMetric + } + if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] { let prediction = predictdata["values"] as! [Double] - PredictionLabel.text = bgUnits.toDisplayUnits(String(Int(prediction.last!))) + PredictionLabel.text = Localizer.toDisplayUnits(String(Int(prediction.last!))) PredictionLabel.textColor = UIColor.systemPurple if UserDefaultsRepository.downloadPrediction.value && latestLoopTime < lastLoopTime { predictionData.removeAll() @@ -59,19 +87,23 @@ extension MainViewController { i += 1 } - let predMin = prediction.min() - let predMax = prediction.max() - tableData[9].value = bgUnits.toDisplayUnits(String(predMin!)) + "/" + bgUnits.toDisplayUnits(String(predMax!)) - + if let predMin = prediction.min(), let predMax = prediction.max() { + let formattedMin = Localizer.toDisplayUnits(String(predMin)) + let formattedMax = Localizer.toDisplayUnits(String(predMax)) + let value = "\(formattedMin)/\(formattedMax)" + infoManager.updateInfoData(type: .minMax, value: value) + } + updatePredictionGraph() } } else { predictionData.removeAll() - tableData[9].value = "" + infoManager.clearInfoData(type: .minMax) updatePredictionGraph() } if let recBolus = lastLoopRecord["recommendedBolus"] as? Double { - tableData[8].value = String(format:"%.2fU", recBolus) + let formattedRecBolus = String(format: "%.2fU", recBolus) + infoManager.updateInfoData(type: .recBolus, value: formattedRecBolus) UserDefaultsRepository.deviceRecBolus.value = recBolus } if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] { diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index 75df89c9..46c9ddc4 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -1,92 +1,166 @@ -// -// DeviceStatusOpenAPS.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-05-19. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// DeviceStatusOpenAPS.swift +// LoopFollow +// Created by Jonas Björkert on 2024-05-19. +// Copyright © 2024 Jon Fawcett. All rights reserved. import Foundation import UIKit +import HealthKit extension MainViewController { func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { - if let lastLoopTime = formatter.date(from: (lastDeviceStatus?["created_at"] as! String))?.timeIntervalSince1970 { UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime if lastLoopRecord["failureReason"] != nil { LoopStatusLabel.text = "X" latestLoopStatusString = "X" - if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Loop Failure: X") } } else { - var wasEnacted = false - if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] { - wasEnacted = true - } - - if let iobdata = lastLoopRecord["iob"] as? [String: AnyObject] { - tableData[0].value = String(format: "%.2f", (iobdata["iob"] as! Double)) - latestIOB = String(format: "%.2f", (iobdata["iob"] as! Double)) - } - if let cobdata = lastLoopRecord["enacted"] as? [String: AnyObject] { - tableData[1].value = String(format: "%.0f", cobdata["COB"] as! Double) - latestCOB = String(format: "%.0f", cobdata["COB"] as! Double) - } - if let recbolusdata = lastLoopRecord["enacted"] as? [String: AnyObject], - let insulinReq = recbolusdata["insulinReq"] as? Double { - tableData[8].value = String(format: "%.2fU", insulinReq) - UserDefaultsRepository.deviceRecBolus.value = insulinReq + guard let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] else { + LoopStatusLabel.text = "↻" + latestLoopStatusString = "↻" + evaluateNotLooping(lastLoopTime: lastLoopTime) + return + } + let wasEnacted = true + + var determinedUnit: HKUnit = .milligramsPerDeciliter + + // Determine the unit based on the threshold value since no unit is provided + if let enactedTargetValue = enacted["threshold"] as? Double { + if enactedTargetValue < 40 { + determinedUnit = .millimolesPerLiter + } + } + + // Updated + if let enactedTimestamp = enacted["timestamp"] as? String, + let enactedTime = formatter.date(from: enactedTimestamp)?.timeIntervalSince1970 { + let formattedTime = Localizer.formatTimestampToLocalString(enactedTime) + infoManager.updateInfoData(type: .updated, value: formattedTime) + } + + // ISF + let profileISF = profileManager.currentISF() + var enactedISF: HKQuantity? + if let enactedISFValue = enacted["ISF"] as? Double { + enactedISF = HKQuantity(unit: determinedUnit, doubleValue: enactedISFValue) + } + if let profileISF = profileISF, let enactedISF = enactedISF, profileISF != enactedISF { + infoManager.updateInfoData(type: .isf, firstValue: profileISF, secondValue: enactedISF, separator: .arrow) + } else if let profileISF = profileISF { + infoManager.updateInfoData(type: .isf, value: profileISF) + } + + // Carb Ratio (CR) + let profileCR = profileManager.currentCarbRatio() + var enactedCR: Double? + if let reasonString = enacted["reason"] as? String { + let pattern = "CR: (\\d+(?:\\.\\d+)?)" + if let regex = try? NSRegularExpression(pattern: pattern) { + let nsString = reasonString as NSString + if let match = regex.firstMatch(in: reasonString, range: NSRange(location: 0, length: nsString.length)) { + let crString = nsString.substring(with: match.range(at: 1)) + enactedCR = Double(crString) + } + } + } + + if let profileCR = profileCR, let enactedCR = enactedCR, profileCR != enactedCR { + infoManager.updateInfoData(type: .carbRatio, value: profileCR, enactedValue: enactedCR, separator: .arrow) + } else if let profileCR = profileCR { + infoManager.updateInfoData(type: .carbRatio, value: profileCR) + } + + // IOB + if let iobMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") { + infoManager.updateInfoData(type: .iob, value: iobMetric) + latestIOB = iobMetric + } + + // COB + if let cobMetric = CarbMetric(from: enacted, key: "COB") { + infoManager.updateInfoData(type: .cob, value: cobMetric) + latestCOB = cobMetric + } + + // Insulin Required + if let insulinReqMetric = InsulinMetric(from: enacted, key: "insulinReq") { + infoManager.updateInfoData(type: .recBolus, value: insulinReqMetric) + UserDefaultsRepository.deviceRecBolus.value = insulinReqMetric.value } else { - tableData[8].value = "N/A" UserDefaultsRepository.deviceRecBolus.value = 0 } - - if let autosensdata = lastLoopRecord["enacted"] as? [String: AnyObject] { - let sens = autosensdata["sensitivityRatio"] as! Double * 100.0 - tableData[11].value = String(format: "%.0f", sens) + "%" + + // Autosens + if let sens = enacted["sensitivityRatio"] as? Double { + let formattedSens = String(format: "%.0f", sens * 100.0) + "%" + infoManager.updateInfoData(type: .autosens, value: formattedSens) + } + + // Eventual BG + if let eventualBGValue = enacted["eventualBG"] as? Double { + let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue) + PredictionLabel.text = Localizer.formatQuantity(eventualBGQuantity) + } + + // Target + let profileTargetHigh = profileManager.currentTargetHigh() + var enactedTarget: HKQuantity? + if let enactedTargetValue = enacted["current_target"] as? Double { + enactedTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: enactedTargetValue) } - - if let eventualdata = lastLoopRecord["enacted"] as? [String: AnyObject] { - if let eventualBGValue = eventualdata["eventualBG"] as? NSNumber { - let eventualBGStringValue = String(describing: eventualBGValue) - PredictionLabel.text = bgUnits.toDisplayUnits(eventualBGStringValue) + + if let profileTargetHigh = profileTargetHigh, let enactedTarget = enactedTarget { + let profileTargetHighFormatted = Localizer.formatQuantity(profileTargetHigh) + let enactedTargetFormatted = Localizer.formatQuantity(enactedTarget) + + // Compare formatted values to avoid issues with minor floating-point differences + // Profile target could be in another unit than enacted target + if profileTargetHighFormatted != enactedTargetFormatted { + infoManager.updateInfoData(type: .target, firstValue: profileTargetHigh, secondValue: enactedTarget, separator: .arrow) + } else { + infoManager.updateInfoData(type: .target, value: profileTargetHigh) } } - - var predictioncolor = UIColor.systemGray + + // TDD + if let tddMetric = InsulinMetric(from: enacted, key: "TDD") { + infoManager.updateInfoData(type: .tdd, value: tddMetric) + } + + let predictioncolor = UIColor.systemGray PredictionLabel.textColor = predictioncolor topPredictionBG = UserDefaultsRepository.minBGScale.value - if let enactdata = lastLoopRecord["enacted"] as? [String: AnyObject], - let predbgdata = enactdata["predBGs"] as? [String: AnyObject] { + if let predbgdata = enacted["predBGs"] as? [String: AnyObject] { let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [ ("ZT", "ZT", 12), ("IOB", "Insulin", 13), ("COB", "LoopYellow", 14), ("UAM", "UAM", 15) ] - + var minPredBG = Double.infinity var maxPredBG = -Double.infinity - + for (type, colorName, dataIndex) in predictionTypes { var predictionData = [ShareGlucoseData]() if let graphdata = predbgdata[type] as? [Double] { var predictionTime = lastLoopTime let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) - + for i in 0...toLoad { if i < graphdata.count { let predictionValue = graphdata[i] minPredBG = min(minPredBG, predictionValue) maxPredBG = max(maxPredBG, predictionValue) - + let prediction = ShareGlucoseData(sgv: Int(round(predictionValue)), date: predictionTime, direction: "flat") predictionData.append(prediction) predictionTime += 300 } } } - + let color = UIColor(named: colorName) ?? UIColor.systemPurple updatePredictionGraphGeneric( dataIndex: dataIndex, @@ -95,14 +169,15 @@ extension MainViewController { color: color ) } - + if minPredBG != Double.infinity && maxPredBG != -Double.infinity { - tableData[9].value = "\(bgUnits.toDisplayUnits(String(minPredBG)))/\(bgUnits.toDisplayUnits(String(maxPredBG)))" + let value = "\(Localizer.toDisplayUnits(String(minPredBG)))/\(Localizer.toDisplayUnits(String(maxPredBG)))" + infoManager.updateInfoData(type: .minMax, value: value) } else { - tableData[9].value = "N/A" + infoManager.updateInfoData(type: .minMax, value: "N/A") } } - + if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] { if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 { var lastBGTime = lastLoopTime diff --git a/LoopFollow/Controllers/Nightscout/NSProfile.swift b/LoopFollow/Controllers/Nightscout/NSProfile.swift new file mode 100644 index 00000000..698de56a --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/NSProfile.swift @@ -0,0 +1,50 @@ +// NSProfile.swift +// LoopFollow +// Created by Jonas Björkert on 2024-07-12. +// Copyright © 2024 Jon Fawcett. All rights reserved. + +import Foundation + +struct NSProfile: Decodable { + struct Store: Decodable { + struct BasalEntry: Decodable { + let value: Double + let time: String + let timeAsSeconds: Double + } + struct SensEntry: Decodable { + let value: Double + let time: String + let timeAsSeconds: Double + } + struct CarbRatioEntry: Decodable { + let value: Double + let time: String + let timeAsSeconds: Double + } + struct OverrideEntry: Decodable { + let name: String? + let targetRange: [Double]? + let duration: Int? + let insulinNeedsScaleFactor: Double? + let symbol: String? + } + struct TargetEntry: Decodable { + let value: Double + let time: String + let timeAsSeconds: Double + } + + let basal: [BasalEntry] + let sens: [SensEntry] + let carbratio: [CarbRatioEntry] + let overrides: [OverrideEntry]? + let target_high: [TargetEntry]? + let target_low: [TargetEntry]? + let timezone: String + } + + let store: [String: Store] + let defaultProfile: String + let units: String +} diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index 96a3eac1..d900b4e9 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -27,10 +27,9 @@ extension MainViewController { guard let store = profileData.store["default"] ?? profileData.store["Default"] else { return } - - tableData[12].value = profileData.defaultProfile - infoTable.reloadData() - + profileManager.loadProfile(from: profileData) + infoManager.updateInfoData(type: .profile, value: profileData.defaultProfile) + basalProfile.removeAll() for basalEntry in store.basal { let entry = basalProfileStruct(value: basalEntry.value, time: basalEntry.time, timeAsSeconds: basalEntry.timeAsSeconds) diff --git a/LoopFollow/Controllers/Nightscout/ProfileManager.swift b/LoopFollow/Controllers/Nightscout/ProfileManager.swift new file mode 100644 index 00000000..831c9078 --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/ProfileManager.swift @@ -0,0 +1,120 @@ +// ProfileManager.swift +// LoopFollow +// Created by Jonas Björkert on 2024-07-12. +// Copyright © 2024 Jon Fawcett. All rights reserved. + +import Foundation +import HealthKit + +struct ProfileManager { + var isfSchedule: [TimeValue] + var basalSchedule: [TimeValue] + var carbRatioSchedule: [TimeValue] + var targetLowSchedule: [TimeValue] + var targetHighSchedule: [TimeValue] + var overrides: [Override] + var units: HKUnit + var timezone: String + var defaultProfile: String + + struct TimeValue { + let timeAsSeconds: Int + let value: T + } + + struct Override { + let name: String + let targetRange: [HKQuantity] + let duration: Int + let insulinNeedsScaleFactor: Double + let symbol: String + } + + init() { + self.isfSchedule = [] + self.basalSchedule = [] + self.carbRatioSchedule = [] + self.targetLowSchedule = [] + self.targetHighSchedule = [] + self.overrides = [] + self.units = .millimolesPerLiter + self.timezone = "UTC" + self.defaultProfile = "" + } + + mutating func loadProfile(from profileData: NSProfile) { + guard let store = profileData.store["default"] ?? profileData.store["Default"] else { + return + } + + self.units = profileData.units.lowercased() == "mg/dl" ? .milligramsPerDeciliter : .millimolesPerLiter + self.timezone = store.timezone + self.defaultProfile = profileData.defaultProfile + + self.isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } + self.basalSchedule = store.basal.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) } + self.carbRatioSchedule = store.carbratio.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) } + self.targetLowSchedule = store.target_low?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? [] + self.targetHighSchedule = store.target_high?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? [] + if let overrides = store.overrides { + self.overrides = overrides.map { Override(name: $0.name ?? "", targetRange: $0.targetRange?.map { HKQuantity(unit: self.units, doubleValue: $0) } ?? [], duration: $0.duration ?? 0, insulinNeedsScaleFactor: $0.insulinNeedsScaleFactor ?? 1.0, symbol: $0.symbol ?? "") } + } else { + self.overrides = [] + } + } + + func currentISF() -> HKQuantity? { + return getCurrentValue(from: isfSchedule) + } + + func currentBasal() -> String? { + if let basal = getCurrentValue(from: basalSchedule) { + return Localizer.formatToLocalizedString(basal, maxFractionDigits: 2, minFractionDigits: 0) + } + return nil + } + + func currentCarbRatio() -> Double? { + return getCurrentValue(from: carbRatioSchedule) + } + + func currentTargetLow() -> HKQuantity? { + return getCurrentValue(from: targetLowSchedule) + } + + func currentTargetHigh() -> HKQuantity? { + return getCurrentValue(from: targetHighSchedule) + } + + private func getCurrentValue(from schedule: [TimeValue]) -> T? { + guard !schedule.isEmpty else { return nil } + + let now = Date() + let calendar = Calendar.current + let currentTimeInSeconds = calendar.component(.hour, from: now) * 3600 + + calendar.component(.minute, from: now) * 60 + + calendar.component(.second, from: now) + + var lastValue: T? + for timeValue in schedule { + if currentTimeInSeconds >= timeValue.timeAsSeconds { + lastValue = timeValue.value + } else { + break + } + } + return lastValue + } + + mutating func clear() { + self.isfSchedule = [] + self.basalSchedule = [] + self.carbRatioSchedule = [] + self.targetLowSchedule = [] + self.targetHighSchedule = [] + self.overrides = [] + self.units = HKUnit.millimolesPerLiter + self.timezone = "UTC" + self.defaultProfile = "" + } +} diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index eb7743e2..c8197494 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -34,7 +34,8 @@ extension MainViewController { // NS Sage Response Processor func updateSage(data: [sageData]) { - self.clearLastInfoData(index: 6) + infoManager.clearInfoData(type: .sage) + if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process/Display: SAGE") } if data.count == 0 { return @@ -68,9 +69,9 @@ extension MainViewController { formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale - let formattedDuration = formatter.string(from: secondsAgo) - tableData[6].value = formattedDuration ?? "" + if let formattedDuration = formatter.string(from: secondsAgo) { + infoManager.updateInfoData(type: .sage, value: formattedDuration) + } } - infoTable.reloadData() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift index beb6a515..b41454b3 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift @@ -10,7 +10,8 @@ import Foundation extension MainViewController { // NS Temp Basal Response Processor func processNSBasals(entries: [[String:AnyObject]]) { - self.clearLastInfoData(index: 2) + infoManager.clearInfoData(type: .basal) + if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") } // due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being. basalData.removeAll() @@ -136,12 +137,11 @@ extension MainViewController { //if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() { if i == tempArray.count - 1 && duration == 0.0 { lastEndDot = dateTimeStamp + (30 * 60) - latestBasal = String(format:"%.2f", basalRate) } else { lastEndDot = dateTimeStamp + (duration * 60) - latestBasal = String(format:"%.2f", basalRate) } - + latestBasal = Localizer.formatToLocalizedString(basalRate, maxFractionDigits: 2, minFractionDigits: 0) + // Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy if i < tempArray.count - 1 { let nextEntry = tempArray[i + 1] as [String : AnyObject]? @@ -186,7 +186,7 @@ extension MainViewController { } } - latestBasal = String(format:"%.2f", scheduled) + latestBasal = Localizer.formatToLocalizedString(scheduled, maxFractionDigits: 2, minFractionDigits: 0) // Make the starting dot at the last ending dot let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot)) basalData.append(startDot) @@ -196,11 +196,13 @@ extension MainViewController { basalData.append(endDot) } - tableData[2].value = latestBasal - infoTable.reloadData() if UserDefaultsRepository.graphBasal.value { updateBasalGraph() } - infoTable.reloadData() + + if let profileBasal = profileManager.currentBasal(), profileBasal != latestBasal { + latestBasal = "\(profileBasal) → \(latestBasal)" + } + infoManager.updateInfoData(type: .basal, value: latestBasal) } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index aa680482..988fe3e5 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -88,7 +88,6 @@ extension MainViewController { } let resultString = String(format: "%.0f", totalCarbs) - tableData[10].value = resultString - infoTable.reloadData() + infoManager.updateInfoData(type: .carbsToday, value: resultString) } } diff --git a/LoopFollow/Controllers/StatsView.swift b/LoopFollow/Controllers/StatsView.swift index a39e578b..b5e6c27e 100644 --- a/LoopFollow/Controllers/StatsView.swift +++ b/LoopFollow/Controllers/StatsView.swift @@ -33,7 +33,7 @@ extension MainViewController { statsLowPercent.text = String(format:"%.1f%", stats.percentLow) + "%" statsInRangePercent.text = String(format:"%.1f%", stats.percentRange) + "%" statsHighPercent.text = String(format:"%.1f%", stats.percentHigh) + "%" - statsAvgBG.text = bgUnits.toDisplayUnits(String(format:"%.0f%", stats.avgBG)) + statsAvgBG.text = Localizer.toDisplayUnits(String(format:"%.0f%", stats.avgBG)) if UserDefaultsRepository.useIFCC.value { statsEstA1C.text = String(format:"%.0f%", stats.a1C) } diff --git a/LoopFollow/Extensions/HKUnit+Extensions.swift b/LoopFollow/Extensions/HKUnit+Extensions.swift new file mode 100644 index 00000000..561925cb --- /dev/null +++ b/LoopFollow/Extensions/HKUnit+Extensions.swift @@ -0,0 +1,20 @@ +// +// HKUnit+Extensions.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-15. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation +import HealthKit + +extension HKUnit { + public static let milligramsPerDeciliter: HKUnit = { + return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) + }() + + public static let millimolesPerLiter: HKUnit = { + return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter()) + }() +} diff --git a/LoopFollow/InfoTable/InfoData.swift b/LoopFollow/InfoTable/InfoData.swift new file mode 100644 index 00000000..d8db84c1 --- /dev/null +++ b/LoopFollow/InfoTable/InfoData.swift @@ -0,0 +1,19 @@ +// +// InfoData.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-11. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation + +class InfoData { + var name: String + var value: String + + init(name: String, value: String = "") { + self.name = name + self.value = value + } +} diff --git a/LoopFollow/InfoTable/InfoDataSeparator.swift b/LoopFollow/InfoTable/InfoDataSeparator.swift new file mode 100644 index 00000000..d34b2402 --- /dev/null +++ b/LoopFollow/InfoTable/InfoDataSeparator.swift @@ -0,0 +1,15 @@ +// +// InfoDataSeparator.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-16. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation + +enum InfoDataSeparator: String { + case arrow = "→" + case slash = "/" + case dash = "-" +} diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift new file mode 100644 index 00000000..758168df --- /dev/null +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -0,0 +1,78 @@ +// +// InfoManager.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-11. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation +import UIKit +import HealthKit + +class InfoManager { + var tableData: [InfoData] + weak var tableView: UITableView? + + init(tableView: UITableView) { + self.tableData = InfoType.allCases.map { InfoData(name: $0.name) } + self.tableView = tableView + } + + func updateInfoData(type: InfoType, value: String) { + tableData[type.rawValue].value = value + tableView?.reloadData() + } + + func updateInfoData(type: InfoType, value: HKQuantity) { + let formattedValue = Localizer.formatQuantity(value) + updateInfoData(type: type, value: formattedValue) + } + + func updateInfoData(type: InfoType, firstValue: HKQuantity, secondValue: HKQuantity, separator: InfoDataSeparator) { + let formattedFirstValue = Localizer.formatQuantity(firstValue) + let formattedSecondValue = Localizer.formatQuantity(secondValue) + let combinedValue = "\(formattedFirstValue) \(separator.rawValue) \(formattedSecondValue)" + updateInfoData(type: type, value: combinedValue) + } + + func updateInfoData(type: InfoType, value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) { + let formattedValue = Localizer.formatToLocalizedString(value, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits) + updateInfoData(type: type, value: formattedValue) + } + + func updateInfoData(type: InfoType, value: Double, enactedValue: Double, separator: InfoDataSeparator, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) { + let formattedValue = Localizer.formatToLocalizedString(value, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits) + let formattedEnactedValue = Localizer.formatToLocalizedString(enactedValue, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits) + let separatorString = separator.rawValue + let combinedValue = "\(formattedValue) \(separatorString) \(formattedEnactedValue)" + updateInfoData(type: type, value: combinedValue) + } + + func updateInfoData(type: InfoType, value: Metric) { + let formattedValue = value.formattedValue() + updateInfoData(type: type, value: formattedValue) + } + + func clearInfoData(type: InfoType) { + tableData[type.rawValue].value = "" + tableView?.reloadData() + } + + func clearInfoData(types: [InfoType]) { + for type in types { + tableData[type.rawValue].value = "" + } + tableView?.reloadData() + } + + func numberOfRows() -> Int { + return UserDefaultsRepository.infoSort.value.filter { UserDefaultsRepository.infoVisible.value[$0] }.count + } + + func dataForIndexPath(_ indexPath: IndexPath) -> InfoData { + let sortedAndVisibleIndexes = UserDefaultsRepository.infoSort.value.filter { UserDefaultsRepository.infoVisible.value[$0] } + let infoIndex = sortedAndVisibleIndexes[indexPath.row] + return tableData[infoIndex] + } +} diff --git a/LoopFollow/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift new file mode 100644 index 00000000..9543d077 --- /dev/null +++ b/LoopFollow/InfoTable/InfoType.swift @@ -0,0 +1,49 @@ +// +// InfoType.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-11. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation + +enum InfoType: Int, CaseIterable { + case iob, cob, basal, override, battery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated, tdd + + var name: String { + switch self { + case .iob: return "IOB" + case .cob: return "COB" + case .basal: return "Basal" + case .override: return "Override" + case .battery: return "Battery" + case .pump: return "Pump" + case .sage: return "SAGE" + case .cage: return "CAGE" + case .recBolus: return "Rec. Bolus" + case .minMax: return "Min/Max" + case .carbsToday: return "Carbs today" + case .autosens: return "Autosens" + case .profile: return "Profile" + case .target: return "Target" + case .isf: return "ISF" + case .carbRatio: return "CR" + case .updated: return "Updated" + case .tdd: return "TDD" + } + } + + var defaultVisible: Bool { + switch self { + case .iob, .cob, .basal, .override, .battery, .pump, .sage, .cage, .recBolus, .minMax, .carbsToday: + return true + default: + return false + } + } + + var sortOrder: Int { + return self.rawValue + } +} diff --git a/LoopFollow/Metric/CarbMetric.swift b/LoopFollow/Metric/CarbMetric.swift new file mode 100644 index 00000000..3a95a882 --- /dev/null +++ b/LoopFollow/Metric/CarbMetric.swift @@ -0,0 +1,25 @@ +// +// CarbMetric.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-17. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation + +class CarbMetric: Metric { + init?(from dictionary: [String: AnyObject], key: String) { + guard let value = dictionary[key] as? Double else { + return nil + } + super.init(value: value, maxFractionDigits: 0, minFractionDigits: 0) + } + + init?(from object: AnyObject?, key: String) { + guard let dictionary = object as? [String: AnyObject], let value = dictionary[key] as? Double else { + return nil + } + super.init(value: value, maxFractionDigits: 0, minFractionDigits: 0) + } +} diff --git a/LoopFollow/Metric/InsulinMetric.swift b/LoopFollow/Metric/InsulinMetric.swift new file mode 100644 index 00000000..a06d8578 --- /dev/null +++ b/LoopFollow/Metric/InsulinMetric.swift @@ -0,0 +1,25 @@ +// +// InsulinMetric.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-17. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation + +class InsulinMetric: Metric { + init?(from dictionary: [String: AnyObject], key: String) { + guard let value = dictionary[key] as? Double else { + return nil + } + super.init(value: value, maxFractionDigits: 2, minFractionDigits: 0) + } + + init?(from object: AnyObject?, key: String) { + guard let dictionary = object as? [String: AnyObject], let value = dictionary[key] as? Double else { + return nil + } + super.init(value: value, maxFractionDigits: 2, minFractionDigits: 0) + } +} diff --git a/LoopFollow/Metric/Metric.swift b/LoopFollow/Metric/Metric.swift new file mode 100644 index 00000000..64d9f09b --- /dev/null +++ b/LoopFollow/Metric/Metric.swift @@ -0,0 +1,25 @@ +// +// Metric.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-17. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// + +import Foundation + +class Metric { + var value: Double + var maxFractionDigits: Int + var minFractionDigits: Int + + init(value: Double, maxFractionDigits: Int, minFractionDigits: Int) { + self.value = value + self.maxFractionDigits = maxFractionDigits + self.minFractionDigits = minFractionDigits + } + + func formattedValue() -> String { + return Localizer.formatToLocalizedString(value, maxFractionDigits: maxFractionDigits, minFractionDigits: minFractionDigits) + } +} diff --git a/LoopFollow/ViewControllers/AlarmViewController.swift b/LoopFollow/ViewControllers/AlarmViewController.swift index 12d264fc..d7e2c5a5 100644 --- a/LoopFollow/ViewControllers/AlarmViewController.swift +++ b/LoopFollow/ViewControllers/AlarmViewController.swift @@ -605,7 +605,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertTemporaryBG.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -655,7 +655,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertUrgentLowBG.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -815,7 +815,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertLowBG.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -843,7 +843,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertLowPersistenceMax.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -992,7 +992,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertHighBG.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -1151,7 +1151,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertUrgentHighBG.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -1295,7 +1295,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertFastDropDelta.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -1332,7 +1332,7 @@ class AlarmViewController: FormViewController { row.hidden = "$alertFastDropUseLimit == false" row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -1477,7 +1477,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertFastRiseDelta.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -1514,7 +1514,7 @@ class AlarmViewController: FormViewController { row.hidden = "$alertFastRiseUseLimit == false" row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -1827,7 +1827,7 @@ class AlarmViewController: FormViewController { row.hidden = "$alertNotLoopingUseLimits == false" row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -1842,7 +1842,7 @@ class AlarmViewController: FormViewController { row.hidden = "$alertNotLoopingUseLimits == false" row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -2041,7 +2041,7 @@ class AlarmViewController: FormViewController { row.value = Double(UserDefaultsRepository.alertMissedBolusLowGramsBG.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } diff --git a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift index f387358b..d1393440 100644 --- a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift @@ -220,7 +220,7 @@ class GeneralSettingsViewController: FormViewController { row.value = Double(UserDefaultsRepository.speakLowBGLimit.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } // Visibility depends on either 'speakLowBG' or 'speakProactiveLowBG' being true row.hidden = Condition.function(["speakLowBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in @@ -243,7 +243,7 @@ class GeneralSettingsViewController: FormViewController { row.value = Double(UserDefaultsRepository.speakFastDropDelta.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } // Visibility depends on 'speakProactiveLowBG' being true row.hidden = Condition.function(["speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in @@ -273,7 +273,7 @@ class GeneralSettingsViewController: FormViewController { row.value = Double(UserDefaultsRepository.speakHighBGLimit.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } // Visibility depends on 'speakHighBG' or 'speakProactiveLowBG' being true row.hidden = Condition.function(["speakHighBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in diff --git a/LoopFollow/ViewControllers/GraphSettingsViewController.swift b/LoopFollow/ViewControllers/GraphSettingsViewController.swift index 09fd6d18..fc4cb890 100644 --- a/LoopFollow/ViewControllers/GraphSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GraphSettingsViewController.swift @@ -204,7 +204,7 @@ class GraphSettingsViewController: FormViewController { row.value = Double(UserDefaultsRepository.minBGScale.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in @@ -231,7 +231,7 @@ class GraphSettingsViewController: FormViewController { row.value = Double(UserDefaultsRepository.lowLine.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } @@ -254,7 +254,7 @@ class GraphSettingsViewController: FormViewController { row.value = Double(UserDefaultsRepository.highLine.value) row.displayValueFor = { value in guard let value = value else { return nil } - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } }.onChange { [weak self] row in guard let value = row.value else { return } diff --git a/LoopFollow/ViewControllers/InfoDisplaySettingsViewController.swift b/LoopFollow/ViewControllers/InfoDisplaySettingsViewController.swift index 209b7a06..eb6b26c6 100644 --- a/LoopFollow/ViewControllers/InfoDisplaySettingsViewController.swift +++ b/LoopFollow/ViewControllers/InfoDisplaySettingsViewController.swift @@ -14,17 +14,16 @@ import EventKitUI class InfoDisplaySettingsViewController: FormViewController { var appStateController: AppStateController? - override func viewDidLoad() { print("Display Load") super.viewDidLoad() if UserDefaultsRepository.forceDarkMode.value { overrideUserInterfaceStyle = .dark } - + createForm() } - + private func createForm() { form +++ Section("General") @@ -33,64 +32,62 @@ class InfoDisplaySettingsViewController: FormViewController { row.tag = "hideInfoTable" row.value = UserDefaultsRepository.hideInfoTable.value }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.hideInfoTable.value = value + guard let value = row.value else { return } + UserDefaultsRepository.hideInfoTable.value = value + } + + +++ MultivaluedSection(multivaluedOptions: .Reorder, header: "Information Display Settings", footer: "Arrange/Enable Information Desired") { + + $0.tag = "InfoDisplay" + + for i in 0.. Int { - //return tableData.count - return derivedTableData.count - + guard let infoManager = infoManager else { + return 0 + } + return infoManager.numberOfRows() } - + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath) - let values = derivedTableData[indexPath.row] + let values = infoManager.dataForIndexPath(indexPath) cell.textLabel?.text = values.name cell.detailTextLabel?.text = values.value return cell @@ -512,11 +450,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele showHideNSDetails() } - //Clear the info data before next pull. This ensures we aren't displaying old data if something fails. - func clearLastInfoData(index: Int){ - tableData[index].value = "" - } - func stringFromTimeInterval(interval: TimeInterval) -> String { let interval = Int(interval) let minutes = (interval / 60) % 60 @@ -557,14 +490,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele func updateBadge(val: Int) { if UserDefaultsRepository.appBadge.value { let latestBG = String(val) - UIApplication.shared.applicationIconBadgeNumber = Int(bgUnits.removePeriodAndCommaForBadge(bgUnits.toDisplayUnits(latestBG))) ?? val + UIApplication.shared.applicationIconBadgeNumber = Int(Localizer.removePeriodAndCommaForBadge(Localizer.toDisplayUnits(latestBG))) ?? val } else { UIApplication.shared.applicationIconBadgeNumber = 0 } } - - - + func setBGTextColor() { if bgData.count > 0 { guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } @@ -626,11 +557,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - self.bgData[self.bgData.count - 1].date) / 60 var deltaString = "" if deltaBG < 0 { - deltaString = bgUnits.toDisplayUnits(String(deltaBG)) + deltaString = Localizer.toDisplayUnits(String(deltaBG)) } else { - deltaString = "+" + bgUnits.toDisplayUnits(String(deltaBG)) + deltaString = "+" + Localizer.toDisplayUnits(String(deltaBG)) } let direction = self.bgDirectionGraphic(self.bgData[self.bgData.count - 1].direction ?? "") @@ -641,7 +572,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele if (UserDefaultsRepository.watchLine2.value.count > 1) { eventTitle += "\n" + UserDefaultsRepository.watchLine2.value } - eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: bgUnits.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv))) + eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv))) eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction) eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString) if self.currentOverride != 1.0 { @@ -659,25 +590,15 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele minAgo = String(Int(deltaTime)) + " min" eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60)) } - var cob = "0" - if self.latestCOB != "" { - cob = self.latestCOB - } var basal = "~" if self.latestBasal != "" { basal = self.latestBasal } - var iob = "0" - if self.latestIOB != "" { - iob = self.latestIOB - } eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo) - eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: iob) - eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: cob) + eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: latestIOB?.formattedValue() ?? "0") + eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: latestCOB?.formattedValue() ?? "0") eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal) - - // Delete Events from last 2 hours and 2 hours in future var deleteStartDate = Date().addingTimeInterval(-60*60*2) var deleteEndDate = Date().addingTimeInterval(60*60*2) @@ -723,7 +644,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele { if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 { guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.sendNotification(self, bgVal: bgUnits.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: bgUnits.toDisplayUnits(String(latestDeltaString)), minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG") + snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: Localizer.toDisplayUnits(String(latestDeltaString)), minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG") } } @@ -737,12 +658,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } debug.debugTextView.text += logText } - - - } - - + // General Notifications func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) { diff --git a/LoopFollow/helpers/Chart.swift b/LoopFollow/helpers/Chart.swift index 29fb6049..77873294 100644 --- a/LoopFollow/helpers/Chart.swift +++ b/LoopFollow/helpers/Chart.swift @@ -66,7 +66,7 @@ final class ChartYOverrideValueFormatter: ValueFormatter { final class ChartYMMOLValueFormatter: AxisValueFormatter { func stringForValue(_ value: Double, axis: AxisBase?) -> String { - return bgUnits.toDisplayUnits(String(value)) + return Localizer.toDisplayUnits(String(value)) } } diff --git a/LoopFollow/helpers/Localizer.swift b/LoopFollow/helpers/Localizer.swift new file mode 100644 index 00000000..8e15f13c --- /dev/null +++ b/LoopFollow/helpers/Localizer.swift @@ -0,0 +1,117 @@ +// +// Units.swift +// LoopFollow +// +// Created by Jon Fawcett on 6/22/20. +// Copyright © 2020 Jon Fawcett. All rights reserved. +// + +import Foundation +import HealthKit + +class Localizer { + static func formatToLocalizedString(_ value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) -> String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + numberFormatter.maximumFractionDigits = maxFractionDigits + numberFormatter.minimumFractionDigits = minFractionDigits + numberFormatter.locale = Locale.current + + let numberValue = NSNumber(value: value) + return numberFormatter.string(from: numberValue) ?? String(value) + } + + static func formatQuantity(_ quantity: HKQuantity) -> String { + let unitPreference = UserDefaultsRepository.units.value + + if unitPreference == "mg/dL" { + let valueInMgdL = quantity.doubleValue(for: .milligramsPerDeciliter) + return formatToLocalizedString(valueInMgdL, maxFractionDigits: 0, minFractionDigits: 0) + } else { + let valueInMmolL = quantity.doubleValue(for: .millimolesPerLiter) + return formatToLocalizedString(valueInMmolL, maxFractionDigits: 1, minFractionDigits: 1) + } + } + + static func formatTimestampToLocalString(_ timestamp: TimeInterval) -> String { + let date = Date(timeIntervalSince1970: timestamp) + let dateFormatter = DateFormatter() + dateFormatter.setLocalizedDateFormatFromTemplate("jms") + dateFormatter.locale = Locale.current + dateFormatter.timeZone = TimeZone.current + return dateFormatter.string(from: date) + } + + static func formatLocalDouble(_ value: Double, unit: String? = nil) -> String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + let units = unit ?? UserDefaultsRepository.units.value + + if units == "mg/dL" { + numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL + } else { + numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L + numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed + } + + numberFormatter.locale = Locale.current + + let numberValue = NSNumber(value: value) + return numberFormatter.string(from: numberValue) ?? String(value) + } + + static func toDisplayUnits(_ value: String) -> String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + if UserDefaultsRepository.units.value == "mg/dL" { + numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL + } else { + numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L + numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed + } + + numberFormatter.locale = Locale.current + + if let number = Float(value) { + if UserDefaultsRepository.units.value == "mg/dL" { + let numberValue = NSNumber(value: number) + return numberFormatter.string(from: numberValue) ?? value + } else { + let mmolValue = Double(number) * GlucoseConversion.mgDlToMmolL // Convert number to Double + let numberValue = NSNumber(value: mmolValue) + return numberFormatter.string(from: numberValue) ?? value + } + } + + return value + } + + static func removePeriodAndCommaForBadge(_ value: String) -> String { + var modifiedValue = value + modifiedValue = modifiedValue.replacingOccurrences(of: ".", with: "") + modifiedValue = modifiedValue.replacingOccurrences(of: ",", with: "") + return modifiedValue + } +} + + +extension Float { + + // remove the decimal part of the float if it is ".0" and trim whitespaces + var cleanValue: String { + return self.truncatingRemainder(dividingBy: 1) == 0 + ? String(format: "%5.0f", self).trimmingCharacters(in: CharacterSet.whitespaces) + : String(format: "%5.1f", self).trimmingCharacters(in: CharacterSet.whitespaces) + } + + var roundTo3f: Float { + return round(to: 3) + } + + func round(to places: Int) -> Float { + let divisor = pow(10.0, Float(places)) + return (divisor * self).rounded() / divisor + } +} diff --git a/LoopFollow/helpers/bgUnits.swift b/LoopFollow/helpers/bgUnits.swift deleted file mode 100644 index 10b31cb4..00000000 --- a/LoopFollow/helpers/bgUnits.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// Units.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/22/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// - -import Foundation - - -class bgUnits { - - static func toDisplayUnits(_ value: String) -> String { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .decimal - - if UserDefaultsRepository.units.value == "mg/dL" { - numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL - } else { - numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L - numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed - } - - numberFormatter.locale = Locale.current - - if let number = Float(value) { - if UserDefaultsRepository.units.value == "mg/dL" { - let numberValue = NSNumber(value: number) - return numberFormatter.string(from: numberValue) ?? value - } else { - let mmolValue = Double(number) * GlucoseConversion.mgDlToMmolL // Convert number to Double - let numberValue = NSNumber(value: mmolValue) - return numberFormatter.string(from: numberValue) ?? value - } - } - - return value - } - - static func removePeriodAndCommaForBadge(_ value: String) -> String { - var modifiedValue = value - modifiedValue = modifiedValue.replacingOccurrences(of: ".", with: "") - modifiedValue = modifiedValue.replacingOccurrences(of: ",", with: "") - return modifiedValue - } -} - - -extension Float { - - // remove the decimal part of the float if it is ".0" and trim whitespaces - var cleanValue: String { - return self.truncatingRemainder(dividingBy: 1) == 0 - ? String(format: "%5.0f", self).trimmingCharacters(in: CharacterSet.whitespaces) - : String(format: "%5.1f", self).trimmingCharacters(in: CharacterSet.whitespaces) - } - - var roundTo3f: Float { - return round(to: 3) - } - - func round(to places: Int) -> Float { - let divisor = pow(10.0, Float(places)) - return (divisor * self).rounded() / divisor - } -} diff --git a/LoopFollow/repository/UserDefaults.swift b/LoopFollow/repository/UserDefaults.swift index 5d36e400..48069851 100644 --- a/LoopFollow/repository/UserDefaults.swift +++ b/LoopFollow/repository/UserDefaults.swift @@ -10,33 +10,35 @@ // // - - import Foundation import UIKit class UserDefaultsRepository { - - // DisplayValues total - static let infoDataTotal = UserDefaultsValue(key: "infoDataTotal", default: 0) - static let infoNames = UserDefaultsValue<[String]>(key: "infoNames", default: [ - "IOB", - "COB", - "Basal", - "Override", - "Battery", - "Pump", - "SAGE", - "CAGE", - "Rec. Bolus", - "Min/Max", //Previously "Pred." - "Carbs today", - "Autosens", - "Profile"]) - static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: [0,1,2,3,4,5,6,7,8,9,10,11,12]) - static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: [true,true,true,true,true,true,true,true,true,true,true,false,false]) + static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: InfoType.allCases.map { $0.sortOrder }) + static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: InfoType.allCases.map { $0.defaultVisible }) + + static func synchronizeInfoTypes() { + // Ensure infoSort array is the correct size + var sortArray = infoSort.value + if sortArray.count != InfoType.allCases.count { + sortArray = InfoType.allCases.map { $0.sortOrder } + infoSort.value = sortArray + } + + // Ensure infoVisible array is the correct size + var visibleArray = infoVisible.value + if visibleArray.count < InfoType.allCases.count { + for infoType in InfoType.allCases[visibleArray.count.. InfoType.allCases.count { + visibleArray = Array(visibleArray.prefix(InfoType.allCases.count)) + } + infoVisible.value = visibleArray + } + static let hideInfoTable = UserDefaultsValue(key: "hideInfoTable", default: false) - + // Nightscout Settings static let showNS = UserDefaultsValue(key: "showNS", default: false) static let url = UserDefaultsValue(key: "url", default: "") @@ -107,7 +109,6 @@ class UserDefaultsRepository { static let graphBolus = UserDefaultsValue(key: "graphBolus", default: true) static let graphCarbs = UserDefaultsValue(key: "graphCarbs", default: true) static let debugLog = UserDefaultsValue(key: "debugLog", default: false) - static let alwaysDownloadAllBG = UserDefaultsValue(key: "alwaysDownloadAllBG", default: true) static let bgUpdateDelay = UserDefaultsValue(key: "bgUpdateDelay", default: 10) static let downloadDays = UserDefaultsValue(key: "downloadDays", default: 1)