|
@@ -108,10 +108,15 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
/// Key: app UUID string, Value: (isInstalled, lastChecked)
|
|
/// Key: app UUID string, Value: (isInstalled, lastChecked)
|
|
|
private var appInstallationCache: [String: (isInstalled: Bool, lastChecked: Date)] = [:]
|
|
private var appInstallationCache: [String: (isInstalled: Bool, lastChecked: Date)] = [:]
|
|
|
private let appStatusCacheLock = NSLock()
|
|
private let appStatusCacheLock = NSLock()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// How long to trust cached app status (in seconds)
|
|
/// How long to trust cached app status (in seconds)
|
|
|
private let appStatusCacheTimeout: TimeInterval = 60 // 1 minute
|
|
private let appStatusCacheTimeout: TimeInterval = 60 // 1 minute
|
|
|
|
|
|
|
|
|
|
+ /// Deduplication: Track last prepared data hash to prevent duplicate expensive work
|
|
|
|
|
+ private var lastPreparedDataHash: Int?
|
|
|
|
|
+ private var lastPreparedWatchState: [GarminWatchState]?
|
|
|
|
|
+ private let hashLock = NSLock()
|
|
|
|
|
+
|
|
|
/// Array of Garmin `IQDevice` objects currently tracked.
|
|
/// Array of Garmin `IQDevice` objects currently tracked.
|
|
|
/// Changing this property triggers re-registration and updates persisted devices.
|
|
/// Changing this property triggers re-registration and updates persisted devices.
|
|
|
private(set) var devices: [IQDevice] = [] {
|
|
private(set) var devices: [IQDevice] = [] {
|
|
@@ -216,9 +221,9 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
guard self.areAppsLikelyInstalled() else {
|
|
guard self.areAppsLikelyInstalled() else {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
let loopAgeMinutes = Int(loopAge / 60)
|
|
let loopAgeMinutes = Int(loopAge / 60)
|
|
|
- let watchState = try await self.setupGarminWatchState()
|
|
|
|
|
|
|
+ let watchState = try await self.setupGarminWatchState(triggeredBy: "Glucose-Stale-Loop")
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
|
self.currentSendTrigger = "Glucose-Stale-Loop (\(loopAgeMinutes)m)"
|
|
self.currentSendTrigger = "Glucose-Stale-Loop (\(loopAgeMinutes)m)"
|
|
|
self.sendWatchStateDataImmediately(watchStateData)
|
|
self.sendWatchStateDataImmediately(watchStateData)
|
|
@@ -266,7 +271,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
|
|
|
|
|
Task {
|
|
Task {
|
|
|
do {
|
|
do {
|
|
|
- let watchState = try await self.setupGarminWatchState()
|
|
|
|
|
|
|
+ let watchState = try await self.setupGarminWatchState(triggeredBy: "IOB-Update")
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
|
self.currentSendTrigger = "IOB-Update"
|
|
self.currentSendTrigger = "IOB-Update"
|
|
|
// Use same throttled pipeline as determinations
|
|
// Use same throttled pipeline as determinations
|
|
@@ -346,7 +351,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
|
|
|
|
|
Task {
|
|
Task {
|
|
|
do {
|
|
do {
|
|
|
- let watchState = try await self.setupGarminWatchState()
|
|
|
|
|
|
|
+ let watchState = try await self.setupGarminWatchState(triggeredBy: "Determination")
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
|
self.currentSendTrigger = "Determination"
|
|
self.currentSendTrigger = "Determination"
|
|
|
// Send to subject for additional 2s debouncing before Bluetooth transmission
|
|
// Send to subject for additional 2s debouncing before Bluetooth transmission
|
|
@@ -490,10 +495,102 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
|
|
|
|
|
// MARK: - Watch State Setup
|
|
// MARK: - Watch State Setup
|
|
|
|
|
|
|
|
|
|
+ /// Computes a hash of key data points to detect if watch state preparation would produce identical results.
|
|
|
|
|
+ /// This prevents expensive CoreData fetches and calculations when data hasn't actually changed.
|
|
|
|
|
+ /// - Returns: Hash value representing current state of glucose, IOB, COB, and basal data
|
|
|
|
|
+ private func computeDataHash() async -> Int {
|
|
|
|
|
+ var hasher = Hasher()
|
|
|
|
|
+
|
|
|
|
|
+ do {
|
|
|
|
|
+ // Hash latest glucose reading (most critical data point)
|
|
|
|
|
+ let glucoseIds = try await fetchGlucose(limit: 1)
|
|
|
|
|
+ let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
|
|
|
|
|
+ .getNSManagedObject(with: glucoseIds, context: backgroundContext)
|
|
|
|
|
+
|
|
|
|
|
+ if let latestGlucose = glucoseObjects.first {
|
|
|
|
|
+ await backgroundContext.perform {
|
|
|
|
|
+ hasher.combine(latestGlucose.glucose)
|
|
|
|
|
+ hasher.combine(latestGlucose.date?.timeIntervalSince1970 ?? 0)
|
|
|
|
|
+ hasher.combine(latestGlucose.direction ?? "")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Hash IOB (changes frequently with insulin activity)
|
|
|
|
|
+ if let iob = iobService.currentIOB {
|
|
|
|
|
+ let iobRounded = Double(iob).roundedDouble(toPlaces: 1)
|
|
|
|
|
+ hasher.combine(iobRounded)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Hash latest determination data (includes COB, ISF, eventualBG, sensRatio)
|
|
|
|
|
+ let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
|
|
|
|
|
+ predicate: NSPredicate.enactedDetermination
|
|
|
|
|
+ )
|
|
|
|
|
+ let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
|
|
|
|
|
+ .getNSManagedObject(with: determinationIds, context: backgroundContext)
|
|
|
|
|
+
|
|
|
|
|
+ if let determination = determinationObjects.first {
|
|
|
|
|
+ await backgroundContext.perform {
|
|
|
|
|
+ // Hash COB (rounded to integer)
|
|
|
|
|
+ let cobDouble = Double(determination.cob)
|
|
|
|
|
+ if cobDouble.isFinite, !cobDouble.isNaN {
|
|
|
|
|
+ let cobInt = Int16(cobDouble)
|
|
|
|
|
+ hasher.combine(cobInt)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Hash sensRatio (autoISFratio) with 2 decimal precision
|
|
|
|
|
+ if let sensRatio = determination.autoISFratio {
|
|
|
|
|
+ let sensRatioDouble = Double(truncating: sensRatio as NSNumber)
|
|
|
|
|
+ if sensRatioDouble.isFinite, !sensRatioDouble.isNaN, sensRatioDouble > 0 {
|
|
|
|
|
+ let sensRounded = sensRatioDouble.roundedDouble(toPlaces: 2)
|
|
|
|
|
+ hasher.combine(sensRounded)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Hash ISF (insulinSensitivity)
|
|
|
|
|
+ if let isf = determination.insulinSensitivity as? Int16 {
|
|
|
|
|
+ if isf > 0, isf <= 300 {
|
|
|
|
|
+ hasher.combine(isf)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Hash eventualBG
|
|
|
|
|
+ if let eventualBG = determination.eventualBG as? Int16 {
|
|
|
|
|
+ if eventualBG >= 0, eventualBG <= 500 {
|
|
|
|
|
+ hasher.combine(eventualBG)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Hash current basal rate (from temp basal or profile)
|
|
|
|
|
+ let tempBasalIds = try await fetchTempBasals()
|
|
|
|
|
+ let tempBasalObjects: [PumpEventStored] = try await CoreDataStack.shared
|
|
|
|
|
+ .getNSManagedObject(with: tempBasalIds, context: backgroundContext)
|
|
|
|
|
+
|
|
|
|
|
+ if let latestTempBasal = tempBasalObjects.first {
|
|
|
|
|
+ await backgroundContext.perform {
|
|
|
|
|
+ if let tempBasalData = latestTempBasal.tempBasal,
|
|
|
|
|
+ let rate = tempBasalData.rate
|
|
|
|
|
+ {
|
|
|
|
|
+ let rateRounded = Double(truncating: rate).roundedDouble(toPlaces: 1)
|
|
|
|
|
+ hasher.combine(rateRounded)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ debugGarmin("[\(formatTimeForLog())] ⚠️ Error computing data hash: \(error)")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return hasher.finalize()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// Builds a GarminWatchState array for both Trio and SwissAlpine watchfaces.
|
|
/// Builds a GarminWatchState array for both Trio and SwissAlpine watchfaces.
|
|
|
/// Uses the SwissAlpine numeric format for all data, sent as an array.
|
|
/// Uses the SwissAlpine numeric format for all data, sent as an array.
|
|
|
/// Both watchfaces receive the same data structure with display configuration fields.
|
|
/// Both watchfaces receive the same data structure with display configuration fields.
|
|
|
- func setupGarminWatchState() async throws -> [GarminWatchState] {
|
|
|
|
|
|
|
+ /// - Parameter triggeredBy: Source of the trigger (for logging/debugging purposes)
|
|
|
|
|
+ /// - Returns: Array of GarminWatchState objects ready to be sent to watch
|
|
|
|
|
+ func setupGarminWatchState(triggeredBy: String = #function) async throws -> [GarminWatchState] {
|
|
|
// Skip expensive calculations if no Garmin devices are connected (except in simulator)
|
|
// Skip expensive calculations if no Garmin devices are connected (except in simulator)
|
|
|
#if targetEnvironment(simulator)
|
|
#if targetEnvironment(simulator)
|
|
|
let skipDeviceCheck = true
|
|
let skipDeviceCheck = true
|
|
@@ -506,6 +603,28 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
return []
|
|
return []
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Compute hash of current data to detect if preparation would produce identical results
|
|
|
|
|
+ let currentHash = await computeDataHash()
|
|
|
|
|
+
|
|
|
|
|
+ // Check if data is unchanged
|
|
|
|
|
+ hashLock.lock()
|
|
|
|
|
+ let hashMatches = (currentHash == lastPreparedDataHash)
|
|
|
|
|
+ let hasCachedState = (lastPreparedWatchState != nil)
|
|
|
|
|
+ hashLock.unlock()
|
|
|
|
|
+
|
|
|
|
|
+ if hashMatches, hasCachedState {
|
|
|
|
|
+ if debugWatchState {
|
|
|
|
|
+ debugGarmin(
|
|
|
|
|
+ "[\(formatTimeForLog())] ⏭️ Skipping preparation - data unchanged (hash: \(currentHash)) [Triggered by: \(triggeredBy)]"
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return lastPreparedWatchState!
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if debugWatchState {
|
|
|
|
|
+ debugGarmin("[\(formatTimeForLog())] ⌚️ Preparing data (hash: \(currentHash)) [Triggered by: \(triggeredBy)]")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
do {
|
|
do {
|
|
|
// Optimize glucose fetch based on watchface needs
|
|
// Optimize glucose fetch based on watchface needs
|
|
|
// SwissAlpine: Fetch 24 entries for historical graph (elements 0-23)
|
|
// SwissAlpine: Fetch 24 entries for historical graph (elements 0-23)
|
|
@@ -757,6 +876,12 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
self.logWatchState(watchStates)
|
|
self.logWatchState(watchStates)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Cache the hash and prepared state for deduplication
|
|
|
|
|
+ self.hashLock.lock()
|
|
|
|
|
+ self.lastPreparedDataHash = currentHash
|
|
|
|
|
+ self.lastPreparedWatchState = watchStates
|
|
|
|
|
+ self.hashLock.unlock()
|
|
|
|
|
+
|
|
|
return watchStates
|
|
return watchStates
|
|
|
}
|
|
}
|
|
|
} catch {
|
|
} catch {
|
|
@@ -864,7 +989,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
private func registerDevices(_ devices: [IQDevice]) {
|
|
private func registerDevices(_ devices: [IQDevice]) {
|
|
|
// Clear out old references
|
|
// Clear out old references
|
|
|
watchApps.removeAll()
|
|
watchApps.removeAll()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Clear app installation cache since we're re-registering
|
|
// Clear app installation cache since we're re-registering
|
|
|
appStatusCacheLock.lock()
|
|
appStatusCacheLock.lock()
|
|
|
appInstallationCache.removeAll()
|
|
appInstallationCache.removeAll()
|
|
@@ -1032,12 +1157,12 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
connectIQ?.getAppStatus(app) { [weak self] status in
|
|
connectIQ?.getAppStatus(app) { [weak self] status in
|
|
|
guard let self = self else { return }
|
|
guard let self = self else { return }
|
|
|
let isInstalled = status?.isInstalled == true
|
|
let isInstalled = status?.isInstalled == true
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Update cache with current status
|
|
// Update cache with current status
|
|
|
if let uuid = app.uuid {
|
|
if let uuid = app.uuid {
|
|
|
self.updateAppStatusCache(uuid: uuid, isInstalled: isInstalled)
|
|
self.updateAppStatusCache(uuid: uuid, isInstalled: isInstalled)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
guard isInstalled else {
|
|
guard isInstalled else {
|
|
|
self.debugGarmin("[\(self.formatTimeForLog())] Garmin: App not installed on device: \(app.uuid!)")
|
|
self.debugGarmin("[\(self.formatTimeForLog())] Garmin: App not installed on device: \(app.uuid!)")
|
|
|
return
|
|
return
|
|
@@ -1047,17 +1172,19 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// MARK: - App Status Cache Management
|
|
// MARK: - App Status Cache Management
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Updates the installation status cache for a given app UUID
|
|
/// Updates the installation status cache for a given app UUID
|
|
|
private func updateAppStatusCache(uuid: UUID, isInstalled: Bool) {
|
|
private func updateAppStatusCache(uuid: UUID, isInstalled: Bool) {
|
|
|
appStatusCacheLock.lock()
|
|
appStatusCacheLock.lock()
|
|
|
defer { appStatusCacheLock.unlock() }
|
|
defer { appStatusCacheLock.unlock() }
|
|
|
appInstallationCache[uuid.uuidString] = (isInstalled, Date())
|
|
appInstallationCache[uuid.uuidString] = (isInstalled, Date())
|
|
|
- debugGarmin("[\(formatTimeForLog())] Garmin: Updated app cache - \(uuid) is \(isInstalled ? "installed" : "NOT installed")")
|
|
|
|
|
|
|
+ debugGarmin(
|
|
|
|
|
+ "[\(formatTimeForLog())] Garmin: Updated app cache - \(uuid) is \(isInstalled ? "installed" : "NOT installed")"
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
/// Checks if any Garmin apps are likely to receive data based on cached status and settings
|
|
/// Checks if any Garmin apps are likely to receive data based on cached status and settings
|
|
|
/// Returns true if cache suggests apps will receive data, or if cache is empty (optimistic on first check)
|
|
/// Returns true if cache suggests apps will receive data, or if cache is empty (optimistic on first check)
|
|
|
/// Considers both app installation status AND whether watchface data is disabled
|
|
/// Considers both app installation status AND whether watchface data is disabled
|
|
@@ -1065,24 +1192,26 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
private func areAppsLikelyInstalled() -> Bool {
|
|
private func areAppsLikelyInstalled() -> Bool {
|
|
|
appStatusCacheLock.lock()
|
|
appStatusCacheLock.lock()
|
|
|
defer { appStatusCacheLock.unlock() }
|
|
defer { appStatusCacheLock.unlock() }
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Get current watchface info for disabled check (always accurate, not cached)
|
|
// Get current watchface info for disabled check (always accurate, not cached)
|
|
|
let watchface = currentWatchface
|
|
let watchface = currentWatchface
|
|
|
let watchfaceUUIDString = watchface.watchfaceUUID?.uuidString
|
|
let watchfaceUUIDString = watchface.watchfaceUUID?.uuidString
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// If cache is empty, check if we should be optimistic
|
|
// If cache is empty, check if we should be optimistic
|
|
|
guard !appInstallationCache.isEmpty else {
|
|
guard !appInstallationCache.isEmpty else {
|
|
|
// Even with empty cache, check if watchface data is disabled
|
|
// Even with empty cache, check if watchface data is disabled
|
|
|
// If disabled and no datafield in cache, we know nothing will receive data
|
|
// If disabled and no datafield in cache, we know nothing will receive data
|
|
|
if isWatchfaceDataDisabled {
|
|
if isWatchfaceDataDisabled {
|
|
|
// No cache entries and watchface disabled means likely no receivers
|
|
// No cache entries and watchface disabled means likely no receivers
|
|
|
- debugGarmin("[\(formatTimeForLog())] Garmin: ⏩ Skipping data preparation - watchface disabled, no cache for datafield")
|
|
|
|
|
|
|
+ debugGarmin(
|
|
|
|
|
+ "[\(formatTimeForLog())] Garmin: ⏩ Skipping data preparation - watchface disabled, no cache for datafield"
|
|
|
|
|
+ )
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
// Be optimistic on first check - assume datafield might be installed
|
|
// Be optimistic on first check - assume datafield might be installed
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// Check each app in cache (trust cache indefinitely - no timeout)
|
|
// Check each app in cache (trust cache indefinitely - no timeout)
|
|
|
for (uuidString, status) in appInstallationCache {
|
|
for (uuidString, status) in appInstallationCache {
|
|
|
// If this is the watchface and data is disabled, skip it regardless of cache
|
|
// If this is the watchface and data is disabled, skip it regardless of cache
|
|
@@ -1091,13 +1220,13 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable, @unchecked S
|
|
|
continue // Watchface won't receive data (disabled) - check other apps
|
|
continue // Watchface won't receive data (disabled) - check other apps
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// If app is installed (per cache), we have a receiver
|
|
// If app is installed (per cache), we have a receiver
|
|
|
if status.isInstalled {
|
|
if status.isInstalled {
|
|
|
return true // Found a receiver
|
|
return true // Found a receiver
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// No apps will receive data (either not installed or watchface is disabled)
|
|
// No apps will receive data (either not installed or watchface is disabled)
|
|
|
debugGarmin("[\(formatTimeForLog())] Garmin: ⏩ Skipping data preparation - no apps will receive data (cached)")
|
|
debugGarmin("[\(formatTimeForLog())] Garmin: ⏩ Skipping data preparation - no apps will receive data (cached)")
|
|
|
return false
|
|
return false
|
|
@@ -1333,9 +1462,9 @@ extension BaseGarminManager: IQUIOverrideDelegate, IQDeviceEventDelegate, IQAppM
|
|
|
debugGarmin("[\(self.formatTimeForLog())] ⏩ Skipping status request - no apps installed (cached)")
|
|
debugGarmin("[\(self.formatTimeForLog())] ⏩ Skipping status request - no apps installed (cached)")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
do {
|
|
do {
|
|
|
- let watchState = try await self.setupGarminWatchState()
|
|
|
|
|
|
|
+ let watchState = try await self.setupGarminWatchState(triggeredBy: "Status-Request")
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
|
self.currentSendTrigger = "Status-Request"
|
|
self.currentSendTrigger = "Status-Request"
|
|
|
// Use 30s throttle to prevent status request spam
|
|
// Use 30s throttle to prevent status request spam
|
|
@@ -1415,6 +1544,13 @@ extension BaseGarminManager: SettingsObserver {
|
|
|
// Clear cached determination data after watchface change
|
|
// Clear cached determination data after watchface change
|
|
|
cachedDeterminationData = nil
|
|
cachedDeterminationData = nil
|
|
|
lastWatchfaceChangeTime = Date()
|
|
lastWatchfaceChangeTime = Date()
|
|
|
|
|
+
|
|
|
|
|
+ // Clear hash cache since data format differs between watchfaces
|
|
|
|
|
+ hashLock.lock()
|
|
|
|
|
+ lastPreparedDataHash = nil
|
|
|
|
|
+ lastPreparedWatchState = nil
|
|
|
|
|
+ hashLock.unlock()
|
|
|
|
|
+
|
|
|
debugGarmin("Garmin: Cleared cached determination data due to watchface change")
|
|
debugGarmin("Garmin: Cleared cached determination data due to watchface change")
|
|
|
|
|
|
|
|
registerDevices(devices)
|
|
registerDevices(devices)
|
|
@@ -1440,7 +1576,7 @@ extension BaseGarminManager: SettingsObserver {
|
|
|
debugGarmin("⏩ Skipping immediate settings update - no apps installed (cached)")
|
|
debugGarmin("⏩ Skipping immediate settings update - no apps installed (cached)")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
do {
|
|
do {
|
|
|
// Try to use cached determination data first to avoid CoreData staleness
|
|
// Try to use cached determination data first to avoid CoreData staleness
|
|
|
if let cachedData = self.cachedDeterminationData {
|
|
if let cachedData = self.cachedDeterminationData {
|
|
@@ -1451,7 +1587,7 @@ extension BaseGarminManager: SettingsObserver {
|
|
|
debugGarmin("Garmin: Immediate update sent for units/re-enable change (from cache)")
|
|
debugGarmin("Garmin: Immediate update sent for units/re-enable change (from cache)")
|
|
|
} else {
|
|
} else {
|
|
|
// Fallback to fresh query if no cache available
|
|
// Fallback to fresh query if no cache available
|
|
|
- let watchState = try await self.setupGarminWatchState()
|
|
|
|
|
|
|
+ let watchState = try await self.setupGarminWatchState(triggeredBy: "Settings-Units/Re-enable")
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
|
self.currentSendTrigger = "Settings-Units/Re-enable"
|
|
self.currentSendTrigger = "Settings-Units/Re-enable"
|
|
|
self.sendWatchStateDataImmediately(watchStateData)
|
|
self.sendWatchStateDataImmediately(watchStateData)
|
|
@@ -1474,9 +1610,9 @@ extension BaseGarminManager: SettingsObserver {
|
|
|
debugGarmin("⏩ Skipping throttled settings update - no apps installed (cached)")
|
|
debugGarmin("⏩ Skipping throttled settings update - no apps installed (cached)")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
do {
|
|
do {
|
|
|
- let watchState = try await self.setupGarminWatchState()
|
|
|
|
|
|
|
+ let watchState = try await self.setupGarminWatchState(triggeredBy: "Settings-DataType")
|
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
let watchStateData = try JSONEncoder().encode(watchState)
|
|
|
self.currentSendTrigger = "Settings-DataType"
|
|
self.currentSendTrigger = "Settings-DataType"
|
|
|
// DataType changes use 30s throttling
|
|
// DataType changes use 30s throttling
|