Jonas Björkert il y a 2 ans
Parent
commit
c8d35d2f43

+ 24 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -12,6 +12,10 @@
 		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 */; };
@@ -212,6 +216,10 @@
 		DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = "<group>"; };
 		DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = "<group>"; };
 		DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKUnit+Extensions.swift"; sourceTree = "<group>"; };
+		DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDataSeparator.swift; sourceTree = "<group>"; };
+		DD0C0C672C48529400DBADDF /* Metric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metric.swift; sourceTree = "<group>"; };
+		DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinMetric.swift; sourceTree = "<group>"; };
+		DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbMetric.swift; sourceTree = "<group>"; };
 		DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = "<group>"; };
 		DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = "<group>"; };
 		DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = "<group>"; };
@@ -443,12 +451,23 @@
 			path = Pods;
 			sourceTree = "<group>";
 		};
+		DD0C0C692C4852A100DBADDF /* Metric */ = {
+			isa = PBXGroup;
+			children = (
+				DD0C0C672C48529400DBADDF /* Metric.swift */,
+				DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */,
+				DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */,
+			);
+			path = Metric;
+			sourceTree = "<group>";
+		};
 		DD13BC732C3FD60E0062313B /* InfoTable */ = {
 			isa = PBXGroup;
 			children = (
 				DD13BC742C3FD6200062313B /* InfoType.swift */,
 				DD13BC762C3FD64E0062313B /* InfoData.swift */,
 				DD13BC782C3FE63A0062313B /* InfoManager.swift */,
+				DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */,
 			);
 			path = InfoTable;
 			sourceTree = "<group>";
@@ -671,6 +690,7 @@
 		FC8DEEE32485D1680075863F /* LoopFollow */ = {
 			isa = PBXGroup;
 			children = (
+				DD0C0C692C4852A100DBADDF /* Metric */,
 				DD13BC732C3FD60E0062313B /* InfoTable */,
 				FCC688702489A57C00A0279D /* Loop Follow.entitlements */,
 				FC8DEEE62485D1ED0075863F /* Info.plist */,
@@ -1041,6 +1061,7 @@
 				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 /* Localizer.swift in Sources */,
 				FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */,
@@ -1063,6 +1084,7 @@
 				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 */,
@@ -1070,8 +1092,10 @@
 				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 */,

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

+ 1 - 1
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -83,7 +83,7 @@ extension MainViewController {
         
     // NS Device Status Response Processor
     func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
-        infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio, .updated])
+        infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio, .updated, .recBolus])
 
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
         if jsonDeviceStatus.count == 0 {

+ 13 - 10
LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift

@@ -63,18 +63,21 @@ extension MainViewController {
                     infoManager.updateInfoData(type: .target, value: profileTarget)
                 }
 
-                if let iobdata = lastLoopRecord["iob"] as? [String: AnyObject],
-                   let iobValue = iobdata["iob"] as? Double {
-                    let formattedIOB = String(format: "%.2f", iobValue)
-                    infoManager.updateInfoData(type: .iob, value: formattedIOB)
-                    latestIOB = formattedIOB
+                /*
+                 IOB
+                 */
+                if let insulinMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") {
+                    infoManager.updateInfoData(type: .iob, value: insulinMetric)
+                    latestIOB = insulinMetric
                 }
 
-                if let cobdata = lastLoopRecord["cob"] as? [String: AnyObject],
-                   let cobValue = cobdata["cob"] as? Double {
-                    let formattedCOB = String(format: "%.0f", cobValue)
-                    infoManager.updateInfoData(type: .cob, value: formattedCOB)
-                    latestCOB = formattedCOB
+
+                /*
+                 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] {

+ 54 - 57
LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift

@@ -1,10 +1,7 @@
-//
-//  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
@@ -12,33 +9,33 @@ 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
+                guard let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] else {
+                    LoopStatusLabel.text = "↻"
+                    latestLoopStatusString = "↻"
+                    evaluateNotLooping(lastLoopTime: lastLoopTime)
+                    return
                 }
+                let wasEnacted = true
 
-                var determinedUnit: HKUnit = .millimolesPerLiter
+                var determinedUnit: HKUnit = .milligramsPerDeciliter
 
-                // Determine the unit based on the target value since no unit is provided
-                if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let enactedTargetValue = enacted["current_target"] as? Double {
-                    if enactedTargetValue > 40 {
-                        determinedUnit = .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 = lastLoopRecord["enacted"]?["timestamp"] as? String,
+                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)
@@ -48,13 +45,12 @@ extension MainViewController {
                  ISF
                  */
                 let profileISF = profileManager.currentISF()
-                var enactedISF: String?
-                if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let enactedISFValue = enacted["ISF"] as? Double {
-                    enactedISF = Localizer.formatLocalDouble(enactedISFValue)
+                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, value: "\(profileISF) → \(enactedISF)")
+                    infoManager.updateInfoData(type: .isf, firstValue: profileISF, secondValue: enactedISF, separator: .arrow)
                 } else if let profileISF = profileISF {
                     infoManager.updateInfoData(type: .isf, value: profileISF)
                 }
@@ -63,61 +59,64 @@ extension MainViewController {
                  Carb Ratio (CR)
                  */
                 let profileCR = profileManager.currentCarbRatio()
-                var enactedCR: String?
-                if let reasonString = lastLoopRecord["enacted"]?["reason"] as? String {
+                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))
-                            if let crValue = Double(crString) {
-                                enactedCR = Localizer.formatToLocalizedString(crValue)
-                            }
+                            enactedCR = Double(crString)
                         }
                     }
                 }
 
                 if let profileCR = profileCR, let enactedCR = enactedCR, profileCR != enactedCR {
-                    infoManager.updateInfoData(type: .carbRatio, value: "\(profileCR) → \(enactedCR)")
+                    infoManager.updateInfoData(type: .carbRatio, value: profileCR, enactedValue: enactedCR, separator: .arrow)
                 } else if let profileCR = profileCR {
                     infoManager.updateInfoData(type: .carbRatio, value: profileCR)
                 }
 
-                if let iobdata = lastLoopRecord["iob"] as? [String: AnyObject],
-                   let iobValue = iobdata["iob"] as? Double {
-                    let formattedIOB = String(format: "%.2f", iobValue)
-                    infoManager.updateInfoData(type: .iob, value: formattedIOB)
-                    latestIOB = formattedIOB
+                /*
+                 IOB
+                 */
+                if let iobMetric = InsulinMetric(from: lastLoopRecord["iob"], key: "iob") {
+                    infoManager.updateInfoData(type: .iob, value: iobMetric)
+                    latestIOB = iobMetric
                 }
 
-                if let cobdata = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let cobValue = cobdata["COB"] as? Double {
-                    let formattedCOB = String(format: "%.0f", cobValue)
-                    infoManager.updateInfoData(type: .cob, value: formattedCOB)
-                    latestCOB = formattedCOB
+                /*
+                 COB
+                 */
+                if let cobMetric = CarbMetric(from: enacted, key: "COB") {
+                    infoManager.updateInfoData(type: .cob, value: cobMetric)
+                    latestCOB = cobMetric
                 }
 
-                if let recbolusdata = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let insulinReq = recbolusdata["insulinReq"] as? Double {
-                    let formattedRecBolus = String(format: "%.2fU", insulinReq)
-                    infoManager.updateInfoData(type: .recBolus, value: formattedRecBolus)
-                    UserDefaultsRepository.deviceRecBolus.value = insulinReq
+                /*
+                 Insulin Required
+                 */
+                if let insulinReqMetric = InsulinMetric(from: enacted, key: "insulinReq") {
+                    infoManager.updateInfoData(type: .recBolus, value: insulinReqMetric)
+                    UserDefaultsRepository.deviceRecBolus.value = insulinReqMetric.value
                 } else {
-                    infoManager.updateInfoData(type: .recBolus, value: "N/A")
                     UserDefaultsRepository.deviceRecBolus.value = 0
                 }
 
-                if let autosensdata = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let sens = autosensdata["sensitivityRatio"] as? Double {
+                /*
+                 Autosens
+                 */
+                if let sens = enacted["sensitivityRatio"] as? Double {
                     let formattedSens = String(format: "%.0f", sens * 100.0) + "%"
                     infoManager.updateInfoData(type: .autosens, value: formattedSens)
                 }
 
-                if let eventualdata = lastLoopRecord["enacted"] as? [String: AnyObject] {
-                    if let eventualBGValue = eventualdata["eventualBG"] as? NSNumber {
-                        let eventualBGStringValue = String(describing: eventualBGValue)
-                        PredictionLabel.text = Localizer.toDisplayUnits(eventualBGStringValue)
-                    }
+                /*
+                 Eventual BG
+                 */
+                if let eventualBGValue = enacted["eventualBG"] as? Double {
+                    let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue)
+                    PredictionLabel.text = Localizer.formatQuantity(eventualBGQuantity)
                 }
 
                 /*
@@ -125,8 +124,7 @@ extension MainViewController {
                  */
                 let profileTargetHigh = profileManager.currentTargetHigh()
                 var enactedTarget: String?
-                if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let enactedTargetValue = enacted["current_target"] as? Double {
+                if let enactedTargetValue = enacted["current_target"] as? Double {
                     enactedTarget = Localizer.toDisplayUnits(String(enactedTargetValue))
                 }
                 if let profileTargetHigh = profileTargetHigh, let enactedTarget = enactedTarget, profileTargetHigh != enactedTarget {
@@ -138,8 +136,7 @@ extension MainViewController {
                 var 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),

+ 6 - 12
LoopFollow/Controllers/Nightscout/ProfileManager.swift

@@ -47,7 +47,7 @@ struct ProfileManager {
             return
         }
 
-        self.units = profileData.units == "mg/dL" ? .milligramsPerDeciliter : .millimolesPerLiter
+        self.units = profileData.units.lowercased() == "mg/dl" ? .milligramsPerDeciliter : .millimolesPerLiter
         self.timezone = store.timezone
         self.defaultProfile = profileData.defaultProfile
 
@@ -63,13 +63,10 @@ struct ProfileManager {
         }
     }
 
-    func currentISF() -> String? {
-        if let isf = getCurrentValue(from: isfSchedule) {
-            return Localizer.formatQuantity(isf)
-        }
-        return nil
+    func currentISF() -> HKQuantity? {
+        return getCurrentValue(from: isfSchedule)
     }
-
+    
     func currentBasal() -> String? {
         if let basal = getCurrentValue(from: basalSchedule) {
             return Localizer.formatToLocalizedString(basal, maxFractionDigits: 2, minFractionDigits: 0)
@@ -77,11 +74,8 @@ struct ProfileManager {
         return nil
     }
 
-    func currentCarbRatio() -> String? {
-        if let carbRatio = getCurrentValue(from: carbRatioSchedule) {
-            return Localizer.formatToLocalizedString(carbRatio, maxFractionDigits: 1, minFractionDigits: 0)
-        }
-        return nil
+    func currentCarbRatio() -> Double? {
+        return getCurrentValue(from: carbRatioSchedule)
     }
 
     func currentTargetLow() -> String? {

+ 15 - 0
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 = "-"
+}

+ 31 - 0
LoopFollow/InfoTable/InfoManager.swift

@@ -8,6 +8,7 @@
 
 import Foundation
 import UIKit
+import HealthKit
 
 class InfoManager {
     var tableData: [InfoData]
@@ -23,6 +24,36 @@ class InfoManager {
         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()

+ 25 - 0
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)
+    }
+}

+ 25 - 0
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)
+    }
+}

+ 25 - 0
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)
+    }
+}

+ 4 - 14
LoopFollow/ViewControllers/MainViewController.swift

@@ -114,10 +114,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     var latestDeltaString = ""
     var latestLoopStatusString = ""
     var latestLoopTime: Double = 0
-    var latestCOB = ""
+    var latestCOB: CarbMetric?
     var latestBasal = ""
     var latestPumpVolume: Double = 50.0
-    var latestIOB = ""
+    var latestIOB: InsulinMetric?
     var lastOverrideStartTime: TimeInterval = 0
     var lastOverrideEndTime: TimeInterval = 0
     
@@ -590,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)