From 9770f74f516f6e4e31cbbb9a6e85dc1c094c668a Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:58:39 +0100 Subject: [PATCH 1/4] tracefile + DeviceStatus processing improvement --- .../CGM/Dexcom/G7/CGMG7Transmitter.swift | 20 +++++++++++-------- .../Managers/Charts/GlucoseChartManager.swift | 2 -- .../Nightscout/NightscoutSyncManager.swift | 12 ++++++----- xdrip/Utilities/Trace.swift | 2 ++ .../RootViewController.swift | 10 ++-------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift b/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift index 6423d2c33..6631fdddc 100644 --- a/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift +++ b/xdrip/BluetoothTransmitter/CGM/Dexcom/G7/CGMG7Transmitter.swift @@ -179,10 +179,6 @@ class CGMG7Transmitter: BluetoothTransmitter, CGMTransmitter { return } - trace("in peripheralDidUpdateValueFor, characteristic uuid = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, characteristic_UUID.description) - - trace("in peripheralDidUpdateValueFor, data = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, value.hexEncodedString()) - if let error = error { trace("error: %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .error , error.localizedDescription) } @@ -210,6 +206,10 @@ class CGMG7Transmitter: BluetoothTransmitter, CGMTransmitter { return } + trace("in peripheralDidUpdateValueFor, characteristic uuid = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, characteristic_UUID.description) + + trace("in peripheralDidUpdateValueFor, data = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, value.hexEncodedString()) + let sensorAgeInDays = Double(round((g7GlucoseMessage.sensorAge / 3600 / 24) * 10) / 10) var maxSensorAgeInDays: Double = 0.0 @@ -269,11 +269,11 @@ class CGMG7Transmitter: BluetoothTransmitter, CGMTransmitter { break case .CBUUID_Backfill: + guard value.count == 9 else { return } - guard value.count == 9 else { - trace(" value.count != 9, no procesing", log: log, category: ConstantsLog.categoryCGMG7, type: .info ) - return - } + trace("in peripheralDidUpdateValueFor, characteristic uuid = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, characteristic_UUID.description) + + trace("in peripheralDidUpdateValueFor, data = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, value.hexEncodedString()) if let sensorAge = sensorAge, sensorAge < (ConstantsDexcomG7.maxSensorAgeInDays * 24 * 3600), let dexcomG7BackfillMessage = DexcomG7BackfillMessage(data: value, sensorAge: sensorAge) { trace(" received backfill mesage, calculatedValue = %{public}@, timeStamp = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, dexcomG7BackfillMessage.calculatedValue.description, dexcomG7BackfillMessage.timeStamp.description(with: .current)) @@ -283,6 +283,10 @@ class CGMG7Transmitter: BluetoothTransmitter, CGMTransmitter { case .CBUUID_Receive_Authentication: + trace("in peripheralDidUpdateValueFor, characteristic uuid = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, characteristic_UUID.description) + + trace("in peripheralDidUpdateValueFor, data = %{public}@", log: log, category: ConstantsLog.categoryCGMG7, type: .info, value.hexEncodedString()) + if let authChallengeRxMessage = AuthChallengeRxMessage(data: value) { if authChallengeRxMessage.paired, authChallengeRxMessage.authenticated { diff --git a/xdrip/Managers/Charts/GlucoseChartManager.swift b/xdrip/Managers/Charts/GlucoseChartManager.swift index d624bbad3..164c6c892 100644 --- a/xdrip/Managers/Charts/GlucoseChartManager.swift +++ b/xdrip/Managers/Charts/GlucoseChartManager.swift @@ -1398,8 +1398,6 @@ public class GlucoseChartManager { trace("in getTreatmentChartPoints, initial calculated max basal = %{public}@, basal scaler = %{public}@", log: self.oslog, category: ConstantsLog.categoryGlucoseChartManager, type: .info, basalRateMaximum.description, basalRateScaler.description) } else if basalRateTreatment.value > basalRateMaximum { basalRateScaler = (ConstantsGlucoseChart.absoluteMinimumChartValueInMgdl - minimumChartValueInMgdl) / basalRateMaximum - - trace("in getTreatmentChartPoints, recalculated max basal = %{public}@, basal scaler = %{public}@", log: self.oslog, category: ConstantsLog.categoryGlucoseChartManager, type: .info, basalRateMaximum.description, basalRateScaler.description) } if let previousBasalRateTreatment = previousBasalRateTreatment { diff --git a/xdrip/Managers/Nightscout/NightscoutSyncManager.swift b/xdrip/Managers/Nightscout/NightscoutSyncManager.swift index bfd1f527d..fb5ae9f2c 100644 --- a/xdrip/Managers/Nightscout/NightscoutSyncManager.swift +++ b/xdrip/Managers/Nightscout/NightscoutSyncManager.swift @@ -524,7 +524,6 @@ public class NightscoutSyncManager: NSObject, ObservableObject { profile.startDate = newStartDate profile.profileName = profileResponse.defaultProfile - profile.createdAt = NightscoutSyncManager.iso8601DateFormatter.date(from: profileResponse.createdAt) ?? .distantPast profile.enteredBy = profileResponse.enteredBy profile.updatedDate = .now } else { @@ -586,7 +585,9 @@ public class NightscoutSyncManager: NSObject, ObservableObject { // just check in case there is something wrong with the dates (i.e. a phone setting change having created future dates) // if we detect this then reset the dates back to force a deviceStatus download and overwrite - if deviceStatus.createdAt > Date() || deviceStatus.updatedDate > Date() || deviceStatus.lastLoopDate > Date() { + // we'll act as if the current date is 20 seconds into the future, just to avoid any differences between timestamps between the Nightscout server and the user's device. + let currentDate = Date().addingTimeInterval(20) + if deviceStatus.createdAt > currentDate || deviceStatus.updatedDate > currentDate || deviceStatus.lastLoopDate > currentDate { deviceStatus.createdAt = .distantPast deviceStatus.updatedDate = .distantPast deviceStatus.lastLoopDate = .distantPast @@ -607,13 +608,13 @@ public class NightscoutSyncManager: NSObject, ObservableObject { // it doesn't matter if it wasn't enacted as that was already handled // check if there is the newly downloaded profile response has an newer date than the stored one // if so, then import it and overwrite the previously stored one - if let deviceStatusResponse = deviceStatusResponseArray.first, let createdAt = NightscoutSyncManager.iso8601DateFormatter.date(from: deviceStatusResponse.createdAt ?? ""), createdAt > deviceStatus.createdAt { - trace("in updateDeviceStatus (openAPS), updating internal device status with new date %{public}@ whilst existing internal device status date was %{public}@", log: self.oslog, category: ConstantsLog.categoryNightscoutSyncManager, type: .info, createdAt.formatted(date: .abbreviated, time: .shortened), deviceStatus.createdAt.formatted(date: .abbreviated, time: .shortened)) + if let deviceStatusResponse = deviceStatusResponseArray.first, let createdAt = NightscoutSyncManager.iso8601DateFormatter.date(from: deviceStatusResponse.createdAt ?? ""), createdAt > deviceStatus.createdAt, deviceStatusResponse.openAPS?.enacted != nil || deviceStatusResponse.openAPS?.suggested != nil { + trace("in updateDeviceStatus (openAPS), updating device status with date %{public}@. Old device status date was %{public}@", log: self.oslog, category: ConstantsLog.categoryNightscoutSyncManager, type: .info, createdAt.formatted(date: .abbreviated, time: .shortened), deviceStatus.createdAt.formatted(date: .abbreviated, time: .shortened)) deviceStatus.updatedDate = .now deviceStatus.createdAt = createdAt - deviceStatus.device = deviceStatusResponse.device ?? "" + deviceStatus.device = deviceStatusResponse.device deviceStatus.id = deviceStatusResponse.id ?? "" deviceStatus.mills = deviceStatusResponse.mills ?? 0 deviceStatus.utcOffset = deviceStatusResponse.utcOffset ?? 0 @@ -675,6 +676,7 @@ public class NightscoutSyncManager: NSObject, ObservableObject { } else { // downloaded profile start date is not newer than the existing profile so ignore it and do nothing + trace("in updateDeviceStatus (openAPS), no new loop cycle received. Exiting deviceStatus update", log: self.oslog, category: ConstantsLog.categoryNightscoutSyncManager, type: .info) return } diff --git a/xdrip/Utilities/Trace.swift b/xdrip/Utilities/Trace.swift index 92dcf53a0..fc44fab25 100644 --- a/xdrip/Utilities/Trace.swift +++ b/xdrip/Utilities/Trace.swift @@ -572,6 +572,8 @@ class Trace { traceInfo.appendStringAndNewLine(" Read from Dexcom app: " + dexcomG5.useOtherApp.description) + traceInfo.appendStringAndNewLine(" Is Anubis? " + dexcomG5.isAnubis.description) + traceInfo.appendStringAndNewLine(" Last reset: " + (dexcomG5.lastResetTimeStamp?.toString(timeStyle: .short, dateStyle: .medium) ?? "nil") + " (" + (dexcomG5.lastResetTimeStamp?.daysAndHoursAgo(appendAgo: true) ?? "nil") + ")") // if needed additional specific info can be added diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 365f520d8..20ff55aa4 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -3775,9 +3775,6 @@ final class RootViewController: UIViewController, ObservableObject { // if there is reasonably recent data, then show values } else if deviceStatus.createdAt > Date().addingTimeInterval(-ConstantsHomeView.loopShowNoDataAfterMinutes) { - // TODO: DEBUG - trace("deviceStatus.createdAt > Date().addingTimeInterval(-ConstantsHomeView.loopShowNoDataAfterMinutes). createdAt = %{public}@ > %{public}@", log: log, category: ConstantsLog.categoryRootView, type: .debug, deviceStatus.createdAt.formatted(date: .abbreviated, time: .standard), Date().addingTimeInterval(-ConstantsHomeView.loopShowNoDataAfterMinutes).formatted(date: .abbreviated, time: .standard)) - updateDeviceStatusValues(showData: true) // reset the text colours (in case they were dimmed when the app went to the background) @@ -3826,9 +3823,6 @@ final class RootViewController: UIViewController, ObservableObject { // so there is no recent data, so hide everything and show red } else { - // TODO: DEBUG - trace("no recent data. createdAt = %{public}@", log: log, category: ConstantsLog.categoryRootView, type: .debug, deviceStatus.createdAt.formatted(date: .abbreviated, time: .standard)) - updateDeviceStatusValues(showData: false) infoStatusActivityIndicatorOutlet.isHidden = true @@ -3839,8 +3833,8 @@ final class RootViewController: UIViewController, ObservableObject { infoStatusButtonOutlet.setTitle(deviceStatus.deviceStatusTitle(), for: .normal) infoStatusButtonOutlet.setTitleColor(deviceStatus.deviceStatusUIColor(), for: .normal) - // TODO: DEBUG - trace("RVC device status error: createdAt = %{public}@, lastChecked = %{public}@, lastLoopDate = %{public}@", log: log, category: ConstantsLog.categoryRootView, type: .debug, nightscoutSyncManager?.deviceStatus.createdAt.formatted(date: .omitted, time: .standard) ?? "nil", nightscoutSyncManager?.deviceStatus.lastCheckedDate.formatted(date: .omitted, time: .standard) ?? "nil", nightscoutSyncManager?.deviceStatus.lastLoopDate.formatted(date: .omitted, time: .standard) ?? "nil") + // only for debug trace file + trace("DeviceStatusUpdate - device status error. createdAt = %{public}@, lastChecked = %{public}@, lastLoopDate = %{public}@", log: log, category: ConstantsLog.categoryRootView, type: .debug, nightscoutSyncManager?.deviceStatus.createdAt.formatted(date: .omitted, time: .standard) ?? "nil", nightscoutSyncManager?.deviceStatus.lastCheckedDate.formatted(date: .omitted, time: .standard) ?? "nil", nightscoutSyncManager?.deviceStatus.lastLoopDate.formatted(date: .omitted, time: .standard) ?? "nil") } } } From c8b68989220a19dd72a1873e9779220131958431 Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Fri, 31 Jan 2025 20:00:49 +0100 Subject: [PATCH 2/4] fix compiler warnings and add fallback SF symbols for (image: Image, color: Color)? { if let percent { switch percent { case 0...10: - return (Image(systemName: "battery.0percent"), Color(.systemRed)) + if #available(iOS 17.0, *) { + return (Image(systemName: "battery.0percent"), Color(.systemRed)) + } else { + return (Image(systemName: "minus.plus.batteryblock.slash"), Color(.systemRed)) + } case 11...25: - return (Image(systemName: "battery.25percent"), Color(.systemYellow)) + if #available(iOS 17.0, *) { + return (Image(systemName: "battery.25percent"), Color(.systemYellow)) + } else { + return (Image(systemName: "minus.plus.batteryblock"), Color(.systemYellow)) + } case 26...65: - return (Image(systemName: "battery.50percent"), Color(.colorSecondary)) + if #available(iOS 17.0, *) { + return (Image(systemName: "battery.50percent"), Color(.colorSecondary)) + } else { + return (Image(systemName: "minus.plus.batteryblock"), Color(.colorSecondary)) + } case 66...90: - return (Image(systemName: "battery.75percent"), Color(.colorSecondary)) + if #available(iOS 17.0, *) { + return (Image(systemName: "battery.75percent"), Color(.colorSecondary)) + } else { + return (Image(systemName: "minus.plus.batteryblock.fill"), Color(.colorSecondary)) + } default: - return (Image(systemName: "battery.100percent"), Color(.colorSecondary)) + if #available(iOS 17.0, *) { + return (Image(systemName: "battery.100percent"), Color(.colorSecondary)) + } else { + return (Image(systemName: "minus.plus.batteryblock.fill"), Color(.colorSecondary)) + } } } diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index bd124eedd..b338c59bc 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -3938,7 +3938,7 @@ BuildIndependentTargetsInParallel = YES; DefaultBuildSystemTypeForWorkspace = Latest; LastSwiftUpdateCheck = 1530; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = "Johan Degraeve"; TargetAttributes = { 4716A4EC2B406C3D00419052 = { @@ -5595,7 +5595,6 @@ 47A6ABEA2B790CC70047A4BA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -5636,7 +5635,6 @@ 47A6ABEB2B790CC70047A4BA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -5874,7 +5872,6 @@ F8AC426F21ADEBD70078C348 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; APP_GROUP_IDENTIFIER_TRIO = "group.org.nightscout.${DEVELOPMENT_TEAM}.trio.trio-app-group"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -5904,7 +5901,6 @@ F8AC427021ADEBD70078C348 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; APP_GROUP_IDENTIFIER = "group.com.${DEVELOPMENT_TEAM}.loopkit.LoopGroup"; APP_GROUP_IDENTIFIER_TRIO = "group.org.nightscout.${DEVELOPMENT_TEAM}.trio.trio-app-group"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; diff --git a/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch App.xcscheme b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch App.xcscheme index fc4410517..0a5f1e95e 100644 --- a/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch App.xcscheme +++ b/xdrip.xcodeproj/xcshareddata/xcschemes/xDrip Watch App.xcscheme @@ -1,6 +1,6 @@ - + + + + + - + Date: Sat, 1 Feb 2025 01:03:38 +0100 Subject: [PATCH 3/4] Fix Nightscout AAPS follow uploader battery parsing --- xdrip/Managers/Nightscout/NightscoutSyncManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xdrip/Managers/Nightscout/NightscoutSyncManager.swift b/xdrip/Managers/Nightscout/NightscoutSyncManager.swift index fb5ae9f2c..f671cc76b 100644 --- a/xdrip/Managers/Nightscout/NightscoutSyncManager.swift +++ b/xdrip/Managers/Nightscout/NightscoutSyncManager.swift @@ -664,8 +664,8 @@ public class NightscoutSyncManager: NSObject, ObservableObject { } if let uploader = deviceStatusResponse.uploader { - deviceStatus.uploaderBatteryPercent = uploader.battery - deviceStatus.uploaderIsCharging = uploader.isCharging + deviceStatus.uploaderBatteryPercent = uploader.battery ?? deviceStatus.uploaderBatteryPercent + deviceStatus.uploaderIsCharging = uploader.isCharging ?? deviceStatus.uploaderIsCharging } // TODO: DEBUG From 172a23cda52fb3f5dcd11976adc18362bd93b5fd Mon Sep 17 00:00:00 2001 From: Paul Plant <37302780+paulplant@users.noreply.github.com> Date: Sat, 8 Feb 2025 18:41:27 +0100 Subject: [PATCH 4/4] Live Activity restart App Intent (Shortcuts) Adds an app intent (LiveActivityIntent) so that an automation in the Shortcuts app can restart an ongoing Live Activity. Usually best done by adding the automation to run during the night to prevent the Live Activity from being cancelled if the app isn't opened in 8 hours. Could also be set to run every 6 hours (as an example) to keep the Live Activity running forever without needing to open the app at all. Co-Authored-By: dugamarian <35396265+dugamarian@users.noreply.github.com> --- xdrip.xcodeproj/project.pbxproj | 4 + xdrip/GlucoseIntent.swift | 66 +++++++------- xdrip/LiveActivityIntent.swift | 28 ++++++ .../LiveActivity/LiveActivityManager.swift | 85 ++++++++++--------- .../RootViewController.swift | 10 +-- 5 files changed, 114 insertions(+), 79 deletions(-) create mode 100644 xdrip/LiveActivityIntent.swift diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index b338c59bc..db4bd1876 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -133,6 +133,7 @@ 4796C6072B9516FD00DE2210 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E51D602448E695001C9E5A /* Bundle.swift */; }; 47976E362BF536BA005E86EC /* AlertSnoozeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47976E352BF536BA005E86EC /* AlertSnoozeStatus.swift */; }; 47976E372BFBB870005E86EC /* TextsAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B3A7B1226A0878004BA588 /* TextsAlerts.swift */; }; + 47A0E6442D4E263800B31C21 /* LiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A0E6432D4E262A00B31C21 /* LiveActivityIntent.swift */; }; 47A6ABE22B790CC60047A4BA /* xDripWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A6ABE12B790CC60047A4BA /* xDripWatchApp.swift */; }; 47A6ABE42B790CC60047A4BA /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A6ABE32B790CC60047A4BA /* MainView.swift */; }; 47A6ABE62B790CC70047A4BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 47A6ABE52B790CC70047A4BA /* Assets.xcassets */; }; @@ -988,6 +989,7 @@ 4798BADC27BA7965002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/LibreNFC.strings; sourceTree = ""; }; 4798BADD27BA7996002583BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/M5StackView.strings; sourceTree = ""; }; 4798BADE27BA79B8002583BC /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/LaunchScreen.strings; sourceTree = ""; }; + 47A0E6432D4E262A00B31C21 /* LiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityIntent.swift; sourceTree = ""; }; 47A6ABDF2B790CC60047A4BA /* xDrip Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "xDrip Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 47A6ABE12B790CC60047A4BA /* xDripWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = xDripWatchApp.swift; sourceTree = ""; }; 47A6ABE32B790CC60047A4BA /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; @@ -2240,6 +2242,7 @@ E4C0061F2B3DE8C100D59303 /* AppIntents */ = { isa = PBXGroup; children = ( + 47A0E6432D4E262A00B31C21 /* LiveActivityIntent.swift */, E4D530622B418FF80018C6A4 /* AppShortcuts.swift */, E4C006202B3DE8EC00D59303 /* GlucoseIntent.swift */, ); @@ -4509,6 +4512,7 @@ 4752B4062635878E0081D551 /* SettingsViewStatisticsSettingsViewModel.swift in Sources */, F897AAF92200F2D200CDDD10 /* CBPeripheralState.swift in Sources */, F858CCED25AE4CD100786B91 /* LibreOOPWebError.swift in Sources */, + 47A0E6442D4E263800B31C21 /* LiveActivityIntent.swift in Sources */, F85542362B7575B40058CE09 /* OmniPodHeartBeatBluetoothPeripheralViewModel.swift in Sources */, F8EE3EAC2B6834FD00B27B96 /* Libre2HeartBeat+CoreDataClass.swift in Sources */, D4FD899727772F9100689788 /* TreatmentEntryAccessor.swift in Sources */, diff --git a/xdrip/GlucoseIntent.swift b/xdrip/GlucoseIntent.swift index eb7996895..b61fabf5c 100644 --- a/xdrip/GlucoseIntent.swift +++ b/xdrip/GlucoseIntent.swift @@ -6,40 +6,33 @@ // Copyright © 2023 Johan Degraeve. All rights reserved. // -#if canImport(AppIntents) import AppIntents -#endif import Foundation import SwiftUI struct GlucoseIntent: AppIntent { + static var title: LocalizedStringResource = "What's my glucose level" + static var description = IntentDescription("Ask to read out your blood glucose level.", categoryName: "Information") + static var authenticationPolicy = IntentAuthenticationPolicy.alwaysAllowed - static var title: LocalizedStringResource { - "What's my glucose level" - } - - static var description: IntentDescription? { - IntentDescription("Ask to read out your blood glucose level.", categoryName: "Information") - } - @MainActor func perform() async throws -> some ReturnsValue & ProvidesDialog & ShowsSnippetView { let coreDataManager = await CoreDataManager.create(for: ConstantsCoreData.modelName) - let bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager) let bgReadings = bgReadingsAccessor.getLatestBgReadings(limit: nil, fromDate: Date(timeIntervalSinceNow: -14400), forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false).sorted { $0.timeStamp < $1.timeStamp } - guard let mostRecent = bgReadings.last else { + guard let latestBgReading = bgReadings.last else { throw IntentError.message("No glucose data") } - let value = mostRecent.calculatedValue.mgDlToMmol(mgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) // UserDefaults.standard.bloodGlucoseUnitIsMgDl ? mostRecent.calculatedValue : (mostRecent.calculatedValue * ConstantsBloodGlucose.mgDlToMmoll) + let isMgDl = UserDefaults.standard.bloodGlucoseUnitIsMgDl - let valueString = value.bgValueToString(mgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl) // UserDefaults.standard.bloodGlucoseUnitIsMgDl ? value.formatted(.number.precision(.fractionLength(0))) : value.formatted(.number.precision(.fractionLength(1))) + let value = latestBgReading.calculatedValue.mgDlToMmol(mgDl: isMgDl) + let valueString = value.bgValueToString(mgDl: isMgDl) - let trendDescription: LocalizedStringResource = switch mostRecent.slopeTrend() { + let trendDescription: LocalizedStringResource = switch latestBgReading.slopeTrend() { case .droppingFast: "dropping fast" case .dropping: "dropping" case .slowlyDropping: "slowly dropping" @@ -58,9 +51,7 @@ struct GlucoseIntent: AppIntent { } var dialogString: IntentDialog = "Your blood glucose is currently \(valueString) and \(trendDescription)" - - let minutesAgo = (bgReadingDates.last?.timeIntervalSinceNow ?? 999 ) / 60 - + let minutesAgo = (bgReadingDates.last?.timeIntervalSinceNow ?? .greatestFiniteMagnitude) / 60 let minutesAgoString = abs(Int(minutesAgo)) if minutesAgo < -30 { @@ -72,7 +63,22 @@ struct GlucoseIntent: AppIntent { return .result( value: value, dialog: dialogString, - view: GlucoseChartView(glucoseChartType: .siriGlucoseIntent, bgReadingValues: bgReadingValues, bgReadingDates: bgReadingDates, isMgDl: UserDefaults.standard.bloodGlucoseUnitIsMgDl, urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, lowLimitInMgDl: UserDefaults.standard.lowMarkValue, highLimitInMgDl: UserDefaults.standard.highMarkValue, urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, liveActivityType: nil, hoursToShowScalingHours: nil, glucoseCircleDiameterScalingHours: nil, overrideChartHeight: nil, overrideChartWidth: nil, highContrast: nil) + view: GlucoseChartView( + glucoseChartType: .siriGlucoseIntent, + bgReadingValues: bgReadingValues, + bgReadingDates: bgReadingDates, + isMgDl: isMgDl, + urgentLowLimitInMgDl: UserDefaults.standard.urgentLowMarkValue, + lowLimitInMgDl: UserDefaults.standard.lowMarkValue, + highLimitInMgDl: UserDefaults.standard.highMarkValue, + urgentHighLimitInMgDl: UserDefaults.standard.urgentHighMarkValue, + liveActivityType: nil, + hoursToShowScalingHours: nil, + glucoseCircleDiameterScalingHours: nil, + overrideChartHeight: nil, + overrideChartWidth: nil, + highContrast: nil + ) ) } } @@ -115,19 +121,19 @@ private extension BgReading { func slopeTrend() -> Trend { switch calculatedValueSlope * 60000 { case ..<(-2): - .droppingFast - case -2 ..< -1: - .dropping - case -1 ..< -0.5: - .slowlyDropping + .droppingFast + case -2 ..< -1: + .dropping + case -1 ..< -0.5: + .slowlyDropping case -0.5 ..< 0.5: - .stable - case 0.5 ..< 1: - .slowlyRising + .stable + case 0.5 ..< 1: + .slowlyRising case 1 ..< 2: - .rising - default: - .risingFast + .rising + default: + .risingFast } } } diff --git a/xdrip/LiveActivityIntent.swift b/xdrip/LiveActivityIntent.swift new file mode 100644 index 000000000..a20002ac2 --- /dev/null +++ b/xdrip/LiveActivityIntent.swift @@ -0,0 +1,28 @@ +// +// LiveActivityIntent.swift +// xdrip +// +// Created by Marian Dugaesescu on 13/10/2024 / Edited by Paul Plant on 06/02/2025. +// Copyright © 2024 Johan Degraeve. All rights reserved. +// + +import AppIntents +import Foundation + +// https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities#Start-and-stop-Live-Activities-from-App-Intents +// https://developer.apple.com/documentation/appintents/liveactivityintent + +/// App Intent used to restart the live activities via Apple Shortcuts automation +/// The user needs to add this as an Automation (for somebody who never opens the app, one automation set every 6 hours is needed, +/// although just one set at 03hrs could be enough for most people to ensure the live activity isn't cancelled during the night) +struct RestartLiveActivityIntent: LiveActivityIntent { + static var title: LocalizedStringResource = "Restart Live Activity" + static var description = IntentDescription("Restarts the glucose monitoring live activity.", categoryName: "Live Activity") + + @MainActor + func perform() async throws -> some IntentResult { + // restart the live activity via the LiveActivityManager singleton + LiveActivityManager.shared.restartActivityFromLiveActivityIntent() + return .result() + } +} diff --git a/xdrip/Managers/LiveActivity/LiveActivityManager.swift b/xdrip/Managers/LiveActivity/LiveActivityManager.swift index 7869133fe..d0b7cbad1 100644 --- a/xdrip/Managers/LiveActivity/LiveActivityManager.swift +++ b/xdrip/Managers/LiveActivity/LiveActivityManager.swift @@ -6,90 +6,99 @@ // Copyright © 2023 Johan Degraeve. All rights reserved. // -import Foundation import ActivityKit +import Foundation import OSLog import SwiftUI /// manager class to handle the live activity events public final class LiveActivityManager { - // MARK: - Private variables - /// for trace - private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLiveActivityManager) private var eventAttributes: XDripWidgetAttributes private var eventActivity: Activity? + // initialize an "empty" contentState and use this to hold the current context state of the live activity after each start/update + // this makes it much easier to restart from an App Intent without needing to generate a new context to send + private var persistentContentState = XDripWidgetAttributes.ContentState(bgReadingValues: [0], bgReadingDates: [.now], isMgDl: true, slopeOrdinal: 0, deltaValueInUserUnit: 0, urgentLowLimitInMgDl: 0, lowLimitInMgDl: 0, highLimitInMgDl: 0, urgentHighLimitInMgDl: 0, liveActivityType: .normal, dataSourceDescription: "", deviceStatusCreatedAt: .now, deviceStatusLastLoopDate: .now) + // the start date of the event so when know track when to proactively end/restart the activity private var eventStartDate: Date + // static shared singleton of LiveActivityManager static let shared = LiveActivityManager() + // for trace + private let log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryLiveActivityManager) + + // initializer - declared as private to prevent outside initialization (due to singleton useage intent) private init() { eventAttributes = XDripWidgetAttributes() eventStartDate = Date() } - } // MARK: - Helper Extension + extension LiveActivityManager { - /// start or update the live activity based upon whether it currently exists or not /// - Parameter contentState: the contentState to show /// - Parameter forceRestart: will force the function to end and restart the live activity func runActivity(contentState: XDripWidgetAttributes.ContentState, forceRestart: Bool) { // checking whether 'Live activities' is enabled for the app in settings if ActivityAuthorizationInfo().areActivitiesEnabled { - // live activities are enabled. Now check if there is a currently // running activity (in which case update it) or if not, start a new one if eventActivity == nil { - trace("in runActivity, starting new live activity", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) - startActivity(contentState: contentState) - } else if forceRestart && eventStartDate < Date().addingTimeInterval(-ConstantsLiveActivity.allowLiveActivityRestartAfterMinutes) { + trace("in runActivity, starting new live activity", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + } else if forceRestart, eventStartDate < Date().addingTimeInterval(-ConstantsLiveActivity.allowLiveActivityRestartAfterMinutes) { // force an end/start cycle of the activity when the app comes to the foreground assuming at least 'x' hours have passed. This restarts the 8 hour limit. - trace("in runActivity, restarting live activity", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) - Task { await endActivity() startActivity(contentState: contentState) } + trace("in runActivity, restarting live activity", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info) } else if eventStartDate < Date().addingTimeInterval(-ConstantsLiveActivity.endLiveActivityAfterMinutes) { // if the activity has been running for almost 8 hours, proactively end the activity before it goes stale - trace("in runActivity, ending live activity on purpose to avoid staying on the screen when stale", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) - Task { await endActivity() } + trace("in runActivity, ending live activity on purpose to avoid staying on the screen when stale", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info) } else { // none of the above conditions are true so let's just update the activity - trace("in runActivity, updating live activity", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) - Task { await updateActivity(to: contentState) } + trace("in runActivity, updating live activity", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info) } } else { - trace("in runActivity, live activities are disabled in the iPhone Settings or permission has not been given.", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + trace("in runActivity, live activities are disabled in the iPhone Settings or permission has not been given.", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info) } } + /// Restart Live Activity from LiveActivityIntent/Shortcut + func restartActivityFromLiveActivityIntent() { + // when intializing the persistentContentState we set all attributes to zero + // we can take advantage of this to check that it has really been updated with a real content state + if persistentContentState.urgentLowLimitInMgDl > 0 { + endAllActivities() + startActivity(contentState: persistentContentState) + trace("in restartActivityFromLiveActivityIntent, restarting live activity from LiveActivityIntent", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + } else { + trace("in restartActivityFromLiveActivityIntent, cannot restart live activity from LiveActivityIntent because there is no persistentContentState available yet", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info) + } + } /// end all live activities that are spawned from the app func endAllActivities() { - // https://developer.apple.com/forums/thread/732418 // Add a semaphore to force it to wait for the activities to end before returning from the method let semaphore = DispatchSemaphore(value: 0) - Task - { + Task { for activity in Activity.activities { - let idString = "\(String(describing: eventActivity?.id))" - trace("Ending live activity: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, idString) + trace("Ending live activity: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, String(describing: eventActivity?.id)) await activity.end(nil, dismissalPolicy: .immediate) } @@ -100,16 +109,14 @@ extension LiveActivityManager { eventActivity = nil } - /// will start a new live activity event based upon the content state passed to the function /// - Parameter contentState: the content state of the new activity private func startActivity(contentState: XDripWidgetAttributes.ContentState) { - - // as we're starting a new activity in the current event, let's set the eventStartDate so we can track how long it has been running eventStartDate = Date() + var updatedContentState = contentState updatedContentState.warnUserToOpenApp = false - updatedContentState.eventStartDate = Date() + updatedContentState.eventStartDate = eventStartDate let content = ActivityContent(state: updatedContentState, staleDate: nil, relevanceScore: 1.0) @@ -119,27 +126,26 @@ extension LiveActivityManager { content: content, pushType: nil ) - let idString = "\(String(describing: eventActivity?.id))" - trace("new live activity started: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, idString) + // update the persistent content state with the new/updated content state + persistentContentState = updatedContentState + + trace("new live activity started: %{public}@", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info, String(describing: eventActivity?.id)) } catch { - trace("error: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, error.localizedDescription) + trace("error: %{public}@", log: log, category: ConstantsLog.categoryLiveActivityManager, type: .info, error.localizedDescription) } } /// update the current live activity /// - Parameter contentState: the updated context state of the activity private func updateActivity(to contentState: XDripWidgetAttributes.ContentState) async { - // check if the activity is dismissed by the user (by swiping away the notification) // if so, then end it completely and start a new one if eventActivity?.activityState == .dismissed { Task { - trace("Previous live activity was dismissed by the user so it will be ended and will try to start a new one.", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) - endAllActivities() - startActivity(contentState: contentState) + trace("Previous live activity was dismissed by the user so it will be ended and will try to start a new one.", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info) } } else { // update the warnUserToOpenApp flag if needed and then update the activity @@ -148,23 +154,20 @@ extension LiveActivityManager { // if the event was started more than 'x' time ago, then let's inform the user that the live activity will soon end so that they open the app updatedContentState.warnUserToOpenApp = eventStartDate < Date().addingTimeInterval(-ConstantsLiveActivity.warnLiveActivityAfterMinutes) ? true : false - let updatedContent = ActivityContent(state: updatedContentState, staleDate: nil) + // update the persistent content state with the new/updated content state + persistentContentState = updatedContentState - await eventActivity?.update(updatedContent) + await eventActivity?.update(ActivityContent(state: updatedContentState, staleDate: nil)) } } /// end the live activity if it is being shown, do nothing if there is no eventyActivity private func endActivity() async { - if eventActivity != nil { Task { - for activity in Activity.activities - { - let idString = "\(String(describing: eventActivity?.id))" - trace("Ending live activity: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, idString) - + for activity in Activity.activities { await activity.end(nil, dismissalPolicy: .immediate) + trace("Ending live activity: %{public}@", log: self.log, category: ConstantsLog.categoryLiveActivityManager, type: .info, String(describing: eventActivity?.id)) } } eventActivity = nil diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index cc59c6a4e..859ee1d01 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -10,9 +10,7 @@ import PieCharts import WatchConnectivity import SwiftUI import WidgetKit -#if canImport(AppIntents) import AppIntents -#endif /// viewcontroller for the home screen final class RootViewController: UIViewController, ObservableObject { @@ -3589,11 +3587,8 @@ final class RootViewController: UIViewController, ObservableObject { // add delta if available if bgReadings.count > 1 { - var previousValueInUserUnit: Double = 0.0 - var actualValueInUserUnit: Double = 0.0 - - previousValueInUserUnit = bgReadings[1].calculatedValue.mgDlToMmol(mgDl: isMgDl) - actualValueInUserUnit = bgReadings[0].calculatedValue.mgDlToMmol(mgDl: isMgDl) + var previousValueInUserUnit: Double = bgReadings[1].calculatedValue.mgDlToMmol(mgDl: isMgDl) + var actualValueInUserUnit: Double = bgReadings[0].calculatedValue.mgDlToMmol(mgDl: isMgDl) // if the values are in mmol/L, then round them to the nearest decimal point in order to get the same precision out of the next operation if !isMgDl { @@ -3602,7 +3597,6 @@ final class RootViewController: UIViewController, ObservableObject { } deltaValueInUserUnit = actualValueInUserUnit - previousValueInUserUnit - slopeOrdinal = bgReadings[0].slopeOrdinal() }