Jonas Björkert 2 лет назад
Родитель
Сommit
76826efc6a

+ 12 - 4
LoopFollow.xcodeproj/project.pbxproj

@@ -9,6 +9,8 @@
 /* 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 */; };
 		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 */; };
@@ -196,7 +198,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 */; };
@@ -206,6 +208,8 @@
 		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 = "<group>"; };
 		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 = "<group>"; };
+		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>"; };
 		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>"; };
@@ -399,7 +403,7 @@
 		FCD49B6B24AA536E007879DC /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = "<group>"; };
 		FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbBolusArrays.swift; sourceTree = "<group>"; };
 		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 = "<group>"; };
+		FCEF87AA24A1417900AE6FA0 /* Localizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizer.swift; sourceTree = "<group>"; };
 		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 = "<group>"; };
 		FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@@ -477,6 +481,8 @@
 				DD493AE82ACF2445009A6922 /* BGData.swift */,
 				DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */,
 				DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */,
+				DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */,
+				DD0C0C612C4175FD00DBADDF /* NSProfile.swift */,
 			);
 			path = Nightscout;
 			sourceTree = "<group>";
@@ -721,7 +727,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 */,
@@ -1026,13 +1032,14 @@
 				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 */,
 				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 */,
@@ -1058,6 +1065,7 @@
 				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 */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,

+ 5 - 5
LoopFollow/Controllers/Alarms.swift

@@ -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)"
         }
         

+ 4 - 4
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)

+ 1 - 17
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

+ 6 - 6
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)
             }
             

+ 2 - 2
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])
+        infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf])
 
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
         if jsonDeviceStatus.count == 0 {
@@ -143,7 +143,7 @@ extension MainViewController {
                 }
 
                 oText += " ("
-                oText += bgUnits.toDisplayUnits(String(minValue)) + "-" + bgUnits.toDisplayUnits(String(maxValue)) + ")"
+                oText += Localizer.toDisplayUnits(String(minValue)) + "-" + Localizer.toDisplayUnits(String(maxValue)) + ")"
             }
 
             infoManager.updateInfoData(type: .override, value: oText)

+ 20 - 3
LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift

@@ -29,6 +29,23 @@ extension MainViewController {
                         
                     }
                 }
+
+                /*
+                 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 iobdata = lastLoopRecord["iob"] as? [String: AnyObject],
                    let iobValue = iobdata["iob"] as? Double {
                     let formattedIOB = String(format: "%.2f", iobValue)
@@ -45,7 +62,7 @@ extension MainViewController {
 
                 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()
@@ -66,8 +83,8 @@ extension MainViewController {
                         }
                         
                         if let predMin = prediction.min(), let predMax = prediction.max() {
-                            let formattedMin = bgUnits.toDisplayUnits(String(predMin))
-                            let formattedMax = bgUnits.toDisplayUnits(String(predMax))
+                            let formattedMin = Localizer.toDisplayUnits(String(predMin))
+                            let formattedMax = Localizer.toDisplayUnits(String(predMax))
                             let value = "\(formattedMin)/\(formattedMax)"
                             infoManager.updateInfoData(type: .minMax, value: value)
                         }

+ 56 - 19
LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift

@@ -1,17 +1,9 @@
-//
-//  DeviceStatusOpenAPS.swift
-//  LoopFollow
-//
-//  Created by Jonas Björkert on 2024-05-19.
-//  Copyright © 2024 Jon Fawcett. All rights reserved.
-//
-
 import Foundation
 import UIKit
 
 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 {
@@ -23,7 +15,46 @@ extension MainViewController {
                 if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] {
                     wasEnacted = true
                 }
-                
+
+                /*
+                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)
+                }
+                if let profileISF = profileISF, let enactedISF = enactedISF, profileISF != enactedISF {
+                    infoManager.updateInfoData(type: .isf, value: "\(profileISF) → \(enactedISF)")
+                } else if let profileISF = profileISF {
+                    infoManager.updateInfoData(type: .isf, value: profileISF)
+                }
+
+                /*
+                 Carb Ratio (CR)
+                 */
+                let profileCR = profileManager.currentCarbRatio()
+                var enactedCR: String?
+                if let reasonString = lastLoopRecord["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)
+                            }
+                        }
+                    }
+                }
+
+                if let profileCR = profileCR, let enactedCR = enactedCR, profileCR != enactedCR {
+                    infoManager.updateInfoData(type: .carbRatio, value: "\(profileCR) → \(enactedCR)")
+                } 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)
@@ -57,10 +88,16 @@ extension MainViewController {
                 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)
+                        PredictionLabel.text = Localizer.toDisplayUnits(eventualBGStringValue)
                     }
                 }
-                
+
+                if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
+                   let currentTarget = enacted["current_target"] as? Double {
+                    let formattedTarget = Localizer.toDisplayUnits(String(currentTarget))
+                    infoManager.updateInfoData(type: .target, value: formattedTarget)
+                }
+
                 var predictioncolor = UIColor.systemGray
                 PredictionLabel.textColor = predictioncolor
                 topPredictionBG = UserDefaultsRepository.minBGScale.value
@@ -72,29 +109,29 @@ extension MainViewController {
                         ("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,
@@ -103,9 +140,9 @@ extension MainViewController {
                             color: color
                         )
                     }
-                    
+
                     if minPredBG != Double.infinity && maxPredBG != -Double.infinity {
-                        let 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 {
                         infoManager.updateInfoData(type: .minMax, value: "N/A")

+ 46 - 0
LoopFollow/Controllers/Nightscout/NSProfile.swift

@@ -0,0 +1,46 @@
+//
+//  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?
+        }
+
+        let basal: [BasalEntry]
+        let sens: [SensEntry]
+        let carbratio: [CarbRatioEntry]
+        let overrides: [OverrideEntry]?
+        let timezone: String
+    }
+
+    let store: [String: Store]
+    let defaultProfile: String
+    let units: String
+}

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

@@ -27,7 +27,7 @@ extension MainViewController {
         guard let store = profileData.store["default"] ?? profileData.store["Default"] else {
             return
         }
-                
+        profileManager.loadProfile(from: profileData)
         infoManager.updateInfoData(type: .profile, value: profileData.defaultProfile)
         
         basalProfile.removeAll()

+ 122 - 0
LoopFollow/Controllers/Nightscout/ProfileManager.swift

@@ -0,0 +1,122 @@
+//
+//  ProfileManager.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-12.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+struct ProfileManager {
+    var isfSchedule: [TimeValue]
+    var basalSchedule: [TimeValue]
+    var carbRatioSchedule: [TimeValue]
+    var overrides: [Override]
+    var units: String
+    var timezone: String
+    var defaultProfile: String
+
+    struct TimeValue {
+        let timeAsSeconds: Int
+        let value: Double
+    }
+
+    struct Override {
+        let name: String
+        let targetRange: [Double]
+        let duration: Int
+        let insulinNeedsScaleFactor: Double
+        let symbol: String
+    }
+
+    init() {
+        // Initialize with default values
+        self.isfSchedule = []
+        self.basalSchedule = []
+        self.carbRatioSchedule = []
+        self.overrides = []
+        self.units = "mmol/L"
+        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
+        self.timezone = store.timezone
+        self.defaultProfile = profileData.defaultProfile
+
+        self.isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $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) }
+        if let overrides = store.overrides {
+            self.overrides = overrides.map { Override(name: $0.name ?? "", targetRange: $0.targetRange ?? [], duration: $0.duration ?? 0, insulinNeedsScaleFactor: $0.insulinNeedsScaleFactor ?? 1.0, symbol: $0.symbol ?? "") }
+        } else {
+            self.overrides = []
+        }
+    }
+
+    func currentISF() -> String? {
+        if let isf = getCurrentValue(from: isfSchedule) {
+            return Localizer.formatLocalDouble(isf, unit: self.units)
+        }
+        return nil
+    }
+
+    //TODO: Formatting
+    func currentBasal() -> String? {
+        if let basal = getCurrentValue(from: basalSchedule) {
+            return formatValue(basal)
+        }
+        return nil
+    }
+
+    func currentCarbRatio() -> String? {
+        if let carbRatio = getCurrentValue(from: carbRatioSchedule) {
+            return Localizer.formatToLocalizedString(carbRatio)
+        }
+        return nil
+    }
+
+    private func getCurrentValue(from schedule: [TimeValue]) -> Double? {
+        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: Double?
+        for timeValue in schedule {
+            if currentTimeInSeconds >= timeValue.timeAsSeconds {
+                lastValue = timeValue.value
+            } else {
+                break
+            }
+        }
+        return lastValue
+    }
+
+    private func formatValue(_ value: Double) -> String {
+        if units == "mg/dL" {
+            return String(format: "%.0f", value)
+        } else {
+            return String(format: "%.2f", value)
+        }
+    }
+
+    mutating func clear() {
+        self.isfSchedule = []
+        self.basalSchedule = []
+        self.carbRatioSchedule = []
+        self.overrides = []
+        self.units = "mmol/L"
+        self.timezone = "UTC"
+        self.defaultProfile = ""
+    }
+}

+ 1 - 1
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)
             }

+ 7 - 19
LoopFollow/InfoTable/InfoType.swift

@@ -9,22 +9,7 @@
 import Foundation
 
 enum InfoType: Int, CaseIterable {
-/*
-    case iob = 0          // 0
-    case cob = 1          // 1
-    case basal = 2        // 2
-    case override = 3     // 3
-    case battery = 4      // 4
-    case pump = 5         // 5
-    case sage = 6         // 6
-    case cage = 7         // 7
-    case recBolus = 8     // 8
-    case minMax = 9       // 9
-    case carbsToday = 10  // 10
-    case autosens = 11    // 11
-    case profile = 12     // 12
-  */
-    case iob, cob, basal, override, battery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile
+    case iob, cob, basal, override, battery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio
 
     var name: String {
         switch self {
@@ -41,15 +26,18 @@ enum InfoType: Int, CaseIterable {
         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"
         }
     }
 
     var defaultVisible: Bool {
         switch self {
-        case .autosens, .profile:
-            return false
-        default:
+        case .iob, .cob, .basal, .override, .battery, .pump, .sage, .cage, .recBolus, .minMax, .carbsToday:
             return true
+        default:
+            return false
         }
     }
 

+ 13 - 13
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 }

+ 3 - 3
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

+ 3 - 3
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 }

+ 8 - 8
LoopFollow/ViewControllers/MainViewController.swift

@@ -73,7 +73,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     var minAgoTimer = Timer()
     var minAgoTimeInterval: TimeInterval = 1.0
     
-    
     // Check Alarms Timer
     // Don't check within 1 minute of alarm triggering to give the snoozer time to save data
     var checkAlarmTimer = Timer()
@@ -88,9 +87,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     var alarmTimer = Timer()
     var calendarTimer = Timer()
     var graphNowTimer = Timer()
-    
+
     // Info Table Setup
     var infoManager: InfoManager!
+    var profileManager = ProfileManager()
 
     var bgData: [ShareGlucoseData] = []
     var basalProfile: [basalProfileStruct] = []
@@ -148,7 +148,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
 
         // Synchronize info types to ensure arrays are the correct size
         UserDefaultsRepository.synchronizeInfoTypes()
-        
+
         // Reset deprecated settings
         UserDefaultsRepository.debugLog.value = false;
         
@@ -490,7 +490,7 @@ 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
         }
@@ -557,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 ?? "")
         
@@ -572,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 {
@@ -654,7 +654,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")
         }
     }
     

+ 1 - 1
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))
     }
 }
 

+ 31 - 2
LoopFollow/helpers/bgUnits.swift

@@ -9,8 +9,37 @@
 import Foundation
 
 
-class bgUnits {
-    
+class Localizer {
+    static func formatToLocalizedString(_ value: Double) -> String {
+        let numberFormatter = NumberFormatter()
+        numberFormatter.numberStyle = .decimal
+        numberFormatter.maximumFractionDigits = 1
+        numberFormatter.minimumFractionDigits = 0
+        numberFormatter.locale = Locale.current
+
+        let numberValue = NSNumber(value: value)
+        return numberFormatter.string(from: numberValue) ?? String(value)
+    }
+
+    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