From d59e73005eb52c3c58994eea4de03113a86da3be Mon Sep 17 00:00:00 2001 From: Tim Mueller-Seydlitz Date: Fri, 5 Dec 2025 10:02:13 +0100 Subject: [PATCH 1/4] Improve resilience on real watches Signed-off-by: Tim Mueller-Seydlitz --- openHAB/WatchMessageService.swift | 3 +- openHABWatch/Domain/UserData.swift | 94 ++++++++++++++++--- openHABWatch/External/AppMessageService.swift | 3 +- 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/openHAB/WatchMessageService.swift b/openHAB/WatchMessageService.swift index dd1eaf2dd..286cd41de 100644 --- a/openHAB/WatchMessageService.swift +++ b/openHAB/WatchMessageService.swift @@ -90,6 +90,7 @@ class WatchMessageService: NSObject, WCSessionDelegate { guard getCachedPreferences() != context else { // avoid updating unchanged preferences + Logger.preferences.debug("⏭️ Preferences unchanged, skipping sync") return } @@ -97,7 +98,7 @@ class WatchMessageService: NSObject, WCSessionDelegate { do { try WCSession.default.updateApplicationContext(context) - Logger.preferences.debug("Successfully updated application context with WatchPreferences.") + Logger.preferences.info("📤 Synced WatchPreferences to watch - sitemapForWatch: \(prefs.sitemapForWatch)") } catch { Logger.preferences.error("Failed to encode or update watch context: \(error.localizedDescription)") } diff --git a/openHABWatch/Domain/UserData.swift b/openHABWatch/Domain/UserData.swift index 1d6467082..fa6939e65 100644 --- a/openHABWatch/Domain/UserData.swift +++ b/openHABWatch/Domain/UserData.swift @@ -106,20 +106,27 @@ final class UserData: ObservableObject { .store(in: &cancellables) // Observe sitemap changes - reload the sitemap when it changes + // Note: We don't use .dropFirst() here because we need to catch the initial value + // when the app context is first received from the iOS app on real devices AppSettings.shared.$sitemapForWatch - .dropFirst() .removeDuplicates() - .debounce(for: .seconds(2.0), scheduler: RunLoop.main) + .debounce(for: .seconds(0.3), scheduler: RunLoop.main) .sink { [weak self] newValue in - guard !newValue.isEmpty else { return } + guard !newValue.isEmpty else { + Logger.userData.debug("Sitemap observer: empty sitemap, ignoring") + return + } + Logger.userData.info("Sitemap observer fired: \(newValue)") Task { @MainActor in guard let self else { return } - // Check if we have an active connection before starting - guard await NetworkTracker.shared.activeConnection != nil else { return } - // Only force restart if the sitemap name actually changed let isDifferentSitemap = self.currentlyLoadingSitemap != newValue + Logger.userData.info("Sitemap change detected - current: \(self.currentlyLoadingSitemap ?? "nil"), new: \(newValue), force: \(isDifferentSitemap)") + + // Note: We don't check for active connection here because NetworkTracker + // can report false negatives (especially during long-polling on real devices). + // The observeNetworkChanges() function will handle retrying when network becomes available. self.startPageHandling(sitemapName: newValue, force: isDifferentSitemap) } } @@ -154,6 +161,8 @@ final class UserData: ObservableObject { continue } + Logger.userData.info("Network connection became available: \(activeConnection.configuration.url)") + if !AppSettings.shared.haveReceivedAppContext { AppMessageService.singleton.requestApplicationContext() errorDescription = NSLocalizedString("settings_not_received", comment: "") @@ -164,10 +173,24 @@ final class UserData: ObservableObject { AppSettings.shared.openHABRootUrl = activeConnection.configuration.url AppSettings.shared.openHABVersion = activeConnection.version - // If we have a sitemap but nothing is loading, start page handling + // Start page handling when network becomes available let sitemapName = AppSettings.shared.sitemapForWatch - if !sitemapName.isEmpty, currentlyLoadingSitemap == nil { - startPageHandling(sitemapName: sitemapName, force: false) + if !sitemapName.isEmpty { + // Check if there's already a running task + if let task = pageHandlingTask, !task.isCancelled { + // Task is running, check if it's for the right sitemap + if currentlyLoadingSitemap == sitemapName { + Logger.userData.info("Page handling task already running for correct sitemap: \(sitemapName)") + } else { + // Running task is for different sitemap, force reload + Logger.userData.info("Page handling task for wrong sitemap, forcing reload: \(sitemapName)") + startPageHandling(sitemapName: sitemapName, force: true) + } + } else { + // No task running or task is cancelled - start a new one + Logger.userData.info("Starting page handling for sitemap: \(sitemapName) after network became available") + startPageHandling(sitemapName: sitemapName, force: false) + } } } } @@ -182,15 +205,20 @@ final class UserData: ObservableObject { } func startPageHandling(sitemapName: String, pageId: String = "", force: Bool = false) { + Logger.userData.info("startPageHandling called - sitemap: \(sitemapName), pageId: \(pageId), force: \(force)") + // Handle concurrent loads based on force parameter if let task = pageHandlingTask, !task.isCancelled { if currentlyLoadingSitemap == sitemapName { if force { + Logger.userData.info("Cancelling existing task for same sitemap (force=true)") task.cancel() } else { + Logger.userData.info("Same sitemap already loading and force=false, skipping") return } } else { + Logger.userData.info("Cancelling existing task for different sitemap") task.cancel() } } @@ -202,16 +230,34 @@ final class UserData: ObservableObject { // Only clear references if this task is still the current one Task { @MainActor in if self.currentlyLoadingSitemap == taskSitemapName { + Logger.userData.debug("Clearing page handling task for: \(taskSitemapName)") self.pageHandlingTask = nil self.currentlyLoadingSitemap = nil } } } + // Get connection configuration - prefer active, fallback to stored local config + var connectionConfig = await NetworkTracker.shared.activeConnection?.configuration + if connectionConfig == nil { + // NetworkTracker can give false negatives on real devices, use stored config + connectionConfig = AppSettings.shared.localConnectionConfig ?? AppSettings.shared.remoteConnectionConfig + Logger.userData.warning("NetworkTracker has no active connection, using stored config: \(connectionConfig?.url ?? "none")") + } + + guard let config = connectionConfig else { + Logger.userData.error("No connection configuration available, cannot load sitemap") + await MainActor.run { + self.errorDescription = "No connection configuration available" + self.showAlert = true + self.isLoadingSitemap = false + } + return + } + do { isLoadingSitemap = true - let activeNetworkConfig = await NetworkTracker.shared.activeConnection?.configuration - let service = try OpenAPIService(connectionConfiguration: activeNetworkConfig ?? ConnectionConfiguration.remoteDefault) + let service = try OpenAPIService(connectionConfiguration: config) let initialPage = try await service.pollDataForPage(sitemapname: sitemapName, pageId: pageId, longPolling: false) try Task.checkCancellation() @@ -267,14 +313,36 @@ final class UserData: ObservableObject { } } } catch { + // Check if this was a network error and we should try remote fallback + let shouldTryRemote = (error as? URLError)?.code == .cannotConnectToHost || + (error as? URLError)?.code == .timedOut || + (error as? URLError)?.code == .networkConnectionLost + await MainActor.run { - Logger.userData.error("Page handling failed with error \(error.localizedDescription)") + Logger.userData.error("Page handling failed for sitemap '\(taskSitemapName)': \(error.localizedDescription)") + Logger.userData.error("Error type: \(String(describing: type(of: error)))") + + // If local connection failed and remote is available, try remote as fallback + if shouldTryRemote, + let remoteConfig = AppSettings.shared.remoteConnectionConfig, + remoteConfig.url != connectionConfig?.url { + Logger.userData.info("Local connection failed, retrying with remote: \(remoteConfig.url)") + // Retry with remote connection + Task { + await MainActor.run { + self.startPageHandling(sitemapName: taskSitemapName, force: true) + } + } + return + } + // Use cached widgets if available instead of clearing completely if self.cachedWidgets.isEmpty { + Logger.userData.warning("No cached widgets available, showing empty state") self.widgets = [] } else { self.widgets = self.cachedWidgets - Logger.userData.info("Using cached widgets during connection failure") + Logger.userData.info("Using \(self.cachedWidgets.count) cached widgets during connection failure") } self.errorDescription = error.localizedDescription self.showAlert = true diff --git a/openHABWatch/External/AppMessageService.swift b/openHABWatch/External/AppMessageService.swift index 238971626..8e0519202 100644 --- a/openHABWatch/External/AppMessageService.swift +++ b/openHABWatch/External/AppMessageService.swift @@ -32,6 +32,7 @@ class AppMessageService: NSObject, WCSessionDelegate { do { // Decode the connection payload let prefs = try JSONDecoder().decode(WatchPreferences.self, from: data) + Logger.preferences.info("📱 Received WatchPreferences - sitemapForWatch: \(prefs.sitemapForWatch), defaultSitemap: \(prefs.defaultSitemap)") AppSettings.shared.localConnectionConfig = prefs.localConnectionConfiguration ?? .localDefault AppSettings.shared.remoteConnectionConfig = prefs.remoteConnectionConfiguration ?? .remoteDefault AppSettings.shared.sitemapName = prefs.defaultSitemap @@ -42,7 +43,7 @@ class AppMessageService: NSObject, WCSessionDelegate { // if let trustedCertificates = applicationContext["trustedCertificates"] as? [String: Data] { // // do we need to do anything here? We load from the shared keychain. // } - Logger.preferences.info("✅ Applied WatchPreferences") + Logger.preferences.info("✅ Applied WatchPreferences - sitemapForWatch now: \(AppSettings.shared.sitemapForWatch)") } catch { Logger.preferences.error("❌ Failed to decode WatchPreferences: \(error.localizedDescription)") } From 8ecc14670e4772564787a69e3454523a62a33b4f Mon Sep 17 00:00:00 2001 From: Tim Mueller-Seydlitz Date: Thu, 11 Dec 2025 14:03:24 +0100 Subject: [PATCH 2/4] Do not react immediately on every handleNetworkChange Signed-off-by: Tim Mueller-Seydlitz --- .../OpenHABCore/Util/NetworkTracker.swift | 15 +++++- openHABWatch/Domain/UserData.swift | 51 +++++-------------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift b/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift index 7d8fde0f5..d70994d1e 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift @@ -457,7 +457,20 @@ public actor NetworkTracker { Logger.networkTracker.info("NetworkTracker: Networkmonitor status: Connected") await checkActiveConnection() } else { - Logger.networkTracker.info("NetworkTracker: Networkmonitor status: Disconnected, stopping connection attempts") + Logger.networkTracker.info("NetworkTracker: Networkmonitor status: Disconnected") + + // Don't immediately clear active connection - path monitor can give false negatives + // especially on devices with VPNs. Test if the connection is actually dead first. + if let currentConnection = activeConnection { + Logger.networkTracker.info("NetworkTracker: Verifying if active connection is still working despite path monitor disconnect...") + let stillWorks = await testConnection(configuration: currentConnection.configuration) + if stillWorks != nil { + Logger.networkTracker.info("NetworkTracker: Active connection still works, ignoring path monitor false negative") + return + } + Logger.networkTracker.info("NetworkTracker: Active connection confirmed dead, clearing and stopping attempts") + } + setActiveConnection(nil) // don´t retry until we have a network connection again retryTask?.cancel() diff --git a/openHABWatch/Domain/UserData.swift b/openHABWatch/Domain/UserData.swift index fa6939e65..7cf1f249f 100644 --- a/openHABWatch/Domain/UserData.swift +++ b/openHABWatch/Domain/UserData.swift @@ -237,27 +237,22 @@ final class UserData: ObservableObject { } } - // Get connection configuration - prefer active, fallback to stored local config - var connectionConfig = await NetworkTracker.shared.activeConnection?.configuration - if connectionConfig == nil { - // NetworkTracker can give false negatives on real devices, use stored config - connectionConfig = AppSettings.shared.localConnectionConfig ?? AppSettings.shared.remoteConnectionConfig - Logger.userData.warning("NetworkTracker has no active connection, using stored config: \(connectionConfig?.url ?? "none")") - } + do { + isLoadingSitemap = true - guard let config = connectionConfig else { - Logger.userData.error("No connection configuration available, cannot load sitemap") - await MainActor.run { - self.errorDescription = "No connection configuration available" - self.showAlert = true - self.isLoadingSitemap = false + // Wait for NetworkTracker to establish a connection (no fallback hacks needed!) + guard let connectionInfo = await NetworkTracker.shared.waitForActiveConnection() else { + Logger.userData.error("No active connection available after timeout") + await MainActor.run { + self.errorDescription = NSLocalizedString("settings_not_received", comment: "") + self.showAlert = true + self.isLoadingSitemap = false + } + return } - return - } - do { - isLoadingSitemap = true - let service = try OpenAPIService(connectionConfiguration: config) + Logger.userData.info("Using connection: \(connectionInfo.configuration.url)") + let service = try OpenAPIService(connectionConfiguration: connectionInfo.configuration) let initialPage = try await service.pollDataForPage(sitemapname: sitemapName, pageId: pageId, longPolling: false) try Task.checkCancellation() @@ -313,29 +308,10 @@ final class UserData: ObservableObject { } } } catch { - // Check if this was a network error and we should try remote fallback - let shouldTryRemote = (error as? URLError)?.code == .cannotConnectToHost || - (error as? URLError)?.code == .timedOut || - (error as? URLError)?.code == .networkConnectionLost - await MainActor.run { Logger.userData.error("Page handling failed for sitemap '\(taskSitemapName)': \(error.localizedDescription)") Logger.userData.error("Error type: \(String(describing: type(of: error)))") - // If local connection failed and remote is available, try remote as fallback - if shouldTryRemote, - let remoteConfig = AppSettings.shared.remoteConnectionConfig, - remoteConfig.url != connectionConfig?.url { - Logger.userData.info("Local connection failed, retrying with remote: \(remoteConfig.url)") - // Retry with remote connection - Task { - await MainActor.run { - self.startPageHandling(sitemapName: taskSitemapName, force: true) - } - } - return - } - // Use cached widgets if available instead of clearing completely if self.cachedWidgets.isEmpty { Logger.userData.warning("No cached widgets available, showing empty state") @@ -348,6 +324,7 @@ final class UserData: ObservableObject { self.showAlert = true self.isLoadingSitemap = false } + // Note: NetworkTracker will automatically handle failover to remote if local fails } } } From 44c834da35c55c4cfe567f55b5d8cb0872c39fd0 Mon Sep 17 00:00:00 2001 From: Tim Mueller-Seydlitz Date: Mon, 15 Dec 2025 10:02:21 +0100 Subject: [PATCH 3/4] .longTerm configuration, Widget update pattern - Update instances instead of replacing, Skip loading number icons Signed-off-by: Tim Mueller-Seydlitz --- .../OpenHABCore/Util/NetworkTracker.swift | 21 ++------ openHABWatch/Domain/UserData.swift | 51 ++++++++++++++++--- openHABWatch/Views/SitemapPageView.swift | 1 - openHABWatch/Views/Utils/IconView.swift | 5 ++ 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift b/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift index d70994d1e..c31250c11 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift @@ -453,25 +453,10 @@ public actor NetworkTracker { return } - if isConnected { - Logger.networkTracker.info("NetworkTracker: Networkmonitor status: Connected") - await checkActiveConnection() - } else { - Logger.networkTracker.info("NetworkTracker: Networkmonitor status: Disconnected") - - // Don't immediately clear active connection - path monitor can give false negatives - // especially on devices with VPNs. Test if the connection is actually dead first. - if let currentConnection = activeConnection { - Logger.networkTracker.info("NetworkTracker: Verifying if active connection is still working despite path monitor disconnect...") - let stillWorks = await testConnection(configuration: currentConnection.configuration) - if stillWorks != nil { - Logger.networkTracker.info("NetworkTracker: Active connection still works, ignoring path monitor false negative") - return - } - Logger.networkTracker.info("NetworkTracker: Active connection confirmed dead, clearing and stopping attempts") - } + Logger.networkTracker.info("NetworkTracker: Networkmonitor status: \(isConnected ? "connected" : "disconnected")") + await checkActiveConnection() - setActiveConnection(nil) + if activeConnection == nil { // don´t retry until we have a network connection again retryTask?.cancel() } diff --git a/openHABWatch/Domain/UserData.swift b/openHABWatch/Domain/UserData.swift index 7cf1f249f..93ae55c40 100644 --- a/openHABWatch/Domain/UserData.swift +++ b/openHABWatch/Domain/UserData.swift @@ -252,7 +252,7 @@ final class UserData: ObservableObject { } Logger.userData.info("Using connection: \(connectionInfo.configuration.url)") - let service = try OpenAPIService(connectionConfiguration: connectionInfo.configuration) + let service = try OpenAPIService(connectionConfiguration: connectionInfo.configuration, serviceConfiguration: .longTerm) let initialPage = try await service.pollDataForPage(sitemapname: sitemapName, pageId: pageId, longPolling: false) try Task.checkCancellation() @@ -264,7 +264,7 @@ final class UserData: ObservableObject { } self.openHABSitemapPage = initialPage let newWidgets = initialPage?.widgets ?? [] - self.widgets = newWidgets + self.updateWidgets(with: newWidgets) if !newWidgets.isEmpty { self.cachedWidgets = newWidgets } @@ -275,19 +275,28 @@ final class UserData: ObservableObject { var backoffAttempt = 0 let maxBackoffDelay: UInt64 = 30_000_000_000 // 30 seconds + Logger.userData.info("Starting long polling loop for sitemap: \(taskSitemapName)") while !Task.isCancelled { do { + Logger.userData.info("Long poll request starting for sitemap: \(taskSitemapName)") + let startTime = Date() + let page = try await service.pollDataForPage(sitemapname: sitemapName, pageId: pageId, longPolling: true) + let elapsed = Date().timeIntervalSince(startTime) try Task.checkCancellation() + Logger.userData.info("Long poll response received for sitemap: \(taskSitemapName) after \(String(format: "%.1f", elapsed))s") await MainActor.run { // Set command handler BEFORE assigning to @Published property to prevent race condition - page?.sendCommand = { [weak self] item, command in - Task { await self?.sendCommand(item, command: command) } + if let page { + page.sendCommand = { [weak self] item, command in + Task { await self?.sendCommand(item, command: command) } + } } self.openHABSitemapPage = page let newWidgets = page?.widgets ?? [] - self.widgets = newWidgets + Logger.userData.info("Updating widgets, received \(newWidgets.count) widgets") + self.updateWidgets(with: newWidgets) if !newWidgets.isEmpty { self.cachedWidgets = newWidgets } @@ -296,17 +305,21 @@ final class UserData: ObservableObject { // Reset backoff after success backoffAttempt = 0 + } catch is CancellationError { + Logger.userData.info("Long polling cancelled for sitemap: \(taskSitemapName)") + throw CancellationError() } catch { backoffAttempt += 1 let baseDelay = min(UInt64(pow(2.0, Double(backoffAttempt))) * 1_000_000_000, maxBackoffDelay) let jitter = UInt64.random(in: 0 ..< (baseDelay / 2)) let totalDelay = baseDelay + jitter - Logger.userData.warning("Polling failed: \(error.localizedDescription). Retrying in \(Double(totalDelay) / 1_000_000_000.0) seconds.") + Logger.userData.warning("Polling failed: \(error.localizedDescription) (\(type(of: error))). Retrying in \(Double(totalDelay) / 1_000_000_000.0) seconds.") try await Task.sleep(nanoseconds: totalDelay) } } + Logger.userData.info("Long polling loop exited for sitemap: \(taskSitemapName)") } catch { await MainActor.run { Logger.userData.error("Page handling failed for sitemap '\(taskSitemapName)': \(error.localizedDescription)") @@ -351,4 +364,30 @@ final class UserData: ObservableObject { showAlert = false startPageHandling(sitemapName: AppSettings.shared.sitemapForWatch) } + + /// Updates existing widget instances instead of replacing them to preserve @ObservedObject references + private func updateWidgets(with newWidgets: [OpenHABWidget]) { + // Build a map of existing widgets by ID for quick lookup + var existingWidgetsMap = Dictionary(uniqueKeysWithValues: widgets.map { ($0.widgetId, $0) }) + var updatedWidgets: [OpenHABWidget] = [] + + for newWidget in newWidgets { + if let existingWidget = existingWidgetsMap[newWidget.widgetId] { + // Update existing widget's properties to preserve the instance + existingWidget.label = newWidget.label + existingWidget.icon = newWidget.icon + existingWidget.state = newWidget.state + existingWidget.item = newWidget.item + existingWidget.stateEnumBinding = newWidget.stateEnumBinding + // Add other properties as needed + updatedWidgets.append(existingWidget) + existingWidgetsMap.removeValue(forKey: newWidget.widgetId) + } else { + // New widget, add it + updatedWidgets.append(newWidget) + } + } + + widgets = updatedWidgets + } } diff --git a/openHABWatch/Views/SitemapPageView.swift b/openHABWatch/Views/SitemapPageView.swift index d1cb7d38e..dea298617 100644 --- a/openHABWatch/Views/SitemapPageView.swift +++ b/openHABWatch/Views/SitemapPageView.swift @@ -35,7 +35,6 @@ struct SitemapPageView: View { VStack(spacing: 4) { ForEach(viewModel.widgets) { widget in rowWidget(widget: widget) - .id(widget.widgetId) } if viewModel.isLoadingSitemap { diff --git a/openHABWatch/Views/Utils/IconView.swift b/openHABWatch/Views/Utils/IconView.swift index a75f542d7..d0b135085 100644 --- a/openHABWatch/Views/Utils/IconView.swift +++ b/openHABWatch/Views/Utils/IconView.swift @@ -20,6 +20,11 @@ struct IconView: View { @ObservedObject var settings = AppSettings.shared var iconURL: URL? { + // Skip loading number icons as they don't exist/aren't useful + if widget.icon == "number" { + return nil + } + var iconColor = widget.iconColor if iconColor.isEmpty { iconColor = "#FFFFFF" From 075f2288e48b1dc85a3b46800dacc0cdf89ae29e Mon Sep 17 00:00:00 2001 From: Tim Mueller-Seydlitz Date: Thu, 18 Dec 2025 08:59:22 +0100 Subject: [PATCH 4/4] Reduce logging Signed-off-by: Tim Mueller-Seydlitz --- openHAB/WatchMessageService.swift | 2 +- openHABWatch/Domain/UserData.swift | 33 ++++++++----------- openHABWatch/External/AppMessageService.swift | 2 +- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/openHAB/WatchMessageService.swift b/openHAB/WatchMessageService.swift index 286cd41de..5ac39eb5d 100644 --- a/openHAB/WatchMessageService.swift +++ b/openHAB/WatchMessageService.swift @@ -98,7 +98,7 @@ class WatchMessageService: NSObject, WCSessionDelegate { do { try WCSession.default.updateApplicationContext(context) - Logger.preferences.info("📤 Synced WatchPreferences to watch - sitemapForWatch: \(prefs.sitemapForWatch)") + Logger.preferences.debug("📤 Synced WatchPreferences to watch - sitemapForWatch: \(prefs.sitemapForWatch)") } catch { Logger.preferences.error("Failed to encode or update watch context: \(error.localizedDescription)") } diff --git a/openHABWatch/Domain/UserData.swift b/openHABWatch/Domain/UserData.swift index 93ae55c40..5cd79e9bb 100644 --- a/openHABWatch/Domain/UserData.swift +++ b/openHABWatch/Domain/UserData.swift @@ -116,13 +116,13 @@ final class UserData: ObservableObject { Logger.userData.debug("Sitemap observer: empty sitemap, ignoring") return } - Logger.userData.info("Sitemap observer fired: \(newValue)") + Logger.userData.debug("Sitemap observer fired: \(newValue)") Task { @MainActor in guard let self else { return } // Only force restart if the sitemap name actually changed let isDifferentSitemap = self.currentlyLoadingSitemap != newValue - Logger.userData.info("Sitemap change detected - current: \(self.currentlyLoadingSitemap ?? "nil"), new: \(newValue), force: \(isDifferentSitemap)") + Logger.userData.debug("Sitemap change detected - current: \(self.currentlyLoadingSitemap ?? "nil"), new: \(newValue), force: \(isDifferentSitemap)") // Note: We don't check for active connection here because NetworkTracker // can report false negatives (especially during long-polling on real devices). @@ -161,7 +161,7 @@ final class UserData: ObservableObject { continue } - Logger.userData.info("Network connection became available: \(activeConnection.configuration.url)") + Logger.userData.debug("Network connection became available: \(activeConnection.configuration.url)") if !AppSettings.shared.haveReceivedAppContext { AppMessageService.singleton.requestApplicationContext() @@ -180,15 +180,15 @@ final class UserData: ObservableObject { if let task = pageHandlingTask, !task.isCancelled { // Task is running, check if it's for the right sitemap if currentlyLoadingSitemap == sitemapName { - Logger.userData.info("Page handling task already running for correct sitemap: \(sitemapName)") + Logger.userData.debug("Page handling task already running for correct sitemap: \(sitemapName)") } else { // Running task is for different sitemap, force reload - Logger.userData.info("Page handling task for wrong sitemap, forcing reload: \(sitemapName)") + Logger.userData.debug("Page handling task for wrong sitemap, forcing reload: \(sitemapName)") startPageHandling(sitemapName: sitemapName, force: true) } } else { // No task running or task is cancelled - start a new one - Logger.userData.info("Starting page handling for sitemap: \(sitemapName) after network became available") + Logger.userData.debug("Starting page handling for sitemap: \(sitemapName) after network became available") startPageHandling(sitemapName: sitemapName, force: false) } } @@ -205,20 +205,20 @@ final class UserData: ObservableObject { } func startPageHandling(sitemapName: String, pageId: String = "", force: Bool = false) { - Logger.userData.info("startPageHandling called - sitemap: \(sitemapName), pageId: \(pageId), force: \(force)") + Logger.userData.debug("startPageHandling called - sitemap: \(sitemapName), pageId: \(pageId), force: \(force)") // Handle concurrent loads based on force parameter if let task = pageHandlingTask, !task.isCancelled { if currentlyLoadingSitemap == sitemapName { if force { - Logger.userData.info("Cancelling existing task for same sitemap (force=true)") + Logger.userData.debug("Cancelling existing task for same sitemap (force=true)") task.cancel() } else { - Logger.userData.info("Same sitemap already loading and force=false, skipping") + Logger.userData.debug("Same sitemap already loading and force=false, skipping") return } } else { - Logger.userData.info("Cancelling existing task for different sitemap") + Logger.userData.debug("Cancelling existing task for different sitemap") task.cancel() } } @@ -251,7 +251,7 @@ final class UserData: ObservableObject { return } - Logger.userData.info("Using connection: \(connectionInfo.configuration.url)") + Logger.userData.debug("Using connection: \(connectionInfo.configuration.url)") let service = try OpenAPIService(connectionConfiguration: connectionInfo.configuration, serviceConfiguration: .longTerm) let initialPage = try await service.pollDataForPage(sitemapname: sitemapName, pageId: pageId, longPolling: false) @@ -275,17 +275,12 @@ final class UserData: ObservableObject { var backoffAttempt = 0 let maxBackoffDelay: UInt64 = 30_000_000_000 // 30 seconds - Logger.userData.info("Starting long polling loop for sitemap: \(taskSitemapName)") + Logger.userData.debug("Starting long polling loop for sitemap: \(taskSitemapName)") while !Task.isCancelled { do { - Logger.userData.info("Long poll request starting for sitemap: \(taskSitemapName)") - let startTime = Date() - let page = try await service.pollDataForPage(sitemapname: sitemapName, pageId: pageId, longPolling: true) - let elapsed = Date().timeIntervalSince(startTime) try Task.checkCancellation() - Logger.userData.info("Long poll response received for sitemap: \(taskSitemapName) after \(String(format: "%.1f", elapsed))s") await MainActor.run { // Set command handler BEFORE assigning to @Published property to prevent race condition if let page { @@ -295,7 +290,6 @@ final class UserData: ObservableObject { } self.openHABSitemapPage = page let newWidgets = page?.widgets ?? [] - Logger.userData.info("Updating widgets, received \(newWidgets.count) widgets") self.updateWidgets(with: newWidgets) if !newWidgets.isEmpty { self.cachedWidgets = newWidgets @@ -306,7 +300,7 @@ final class UserData: ObservableObject { backoffAttempt = 0 } catch is CancellationError { - Logger.userData.info("Long polling cancelled for sitemap: \(taskSitemapName)") + Logger.userData.debug("Long polling cancelled for sitemap: \(taskSitemapName)") throw CancellationError() } catch { backoffAttempt += 1 @@ -319,7 +313,6 @@ final class UserData: ObservableObject { try await Task.sleep(nanoseconds: totalDelay) } } - Logger.userData.info("Long polling loop exited for sitemap: \(taskSitemapName)") } catch { await MainActor.run { Logger.userData.error("Page handling failed for sitemap '\(taskSitemapName)': \(error.localizedDescription)") diff --git a/openHABWatch/External/AppMessageService.swift b/openHABWatch/External/AppMessageService.swift index 8e0519202..97fb759d2 100644 --- a/openHABWatch/External/AppMessageService.swift +++ b/openHABWatch/External/AppMessageService.swift @@ -43,7 +43,7 @@ class AppMessageService: NSObject, WCSessionDelegate { // if let trustedCertificates = applicationContext["trustedCertificates"] as? [String: Data] { // // do we need to do anything here? We load from the shared keychain. // } - Logger.preferences.info("✅ Applied WatchPreferences - sitemapForWatch now: \(AppSettings.shared.sitemapForWatch)") + Logger.preferences.debug("✅ Applied WatchPreferences - sitemapForWatch now: \(AppSettings.shared.sitemapForWatch)") } catch { Logger.preferences.error("❌ Failed to decode WatchPreferences: \(error.localizedDescription)") }