Remove deprecated sync activeURL() and make SSID/hardware-address access async#4953
Merged
Conversation
Replaces the deprecated synchronous ConnectionInfo.activeURL() (and the synchronous activeAPIURL()/webhookURL()/isOnInternalNetwork) with async equivalents that refresh network information before evaluating which URL is active, and reworks ConnectivityWrapper so SSID/BSSID/hardware-address access is async-first: - ConnectivityWrapper: new NetworkState snapshot, async currentNetworkState()/currentWiFiSSID()/currentWiFiBSSID()/ currentNetworkHardwareAddress(), coalesced fetches, and a single lastKnownNetworkState() escape hatch for consumers that cannot be async (HAKit's synchronous connectionInfo closure); on Catalyst the last-known state reads macBridge live - ConnectionInfo: async activeURL()/activeAPIURL()/webhookURL()/ isOnInternalNetwork(); synchronous evaluateActiveURL()/ evaluateWebhookURL() remain internal for the sync-constrained core - Server: async activeAPIURL()/webhookURL() siblings of activeURL() - ServerRequestAdapter refreshes network info before adapting requests (and no longer double-calls its completion on missing active URL) - All call sites migrated: WebView URL loading, camera streaming, CarPlay, watch, widgets/app intents, onboarding token exchange (now async throws), AuthenticationAPI/TokenManager/WebhookManager/ HAAPI request helpers bridge PromiseKit via Task - SSID consumers (zone manager, local push diagnostics, sensors, watch context sync, settings, Improv) fetch network state async - Tests updated to the async API and NetworkState-based overrides Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
- Guard Realm zone objects against invalidation across the new async SSID fetch gap (ZoneManagerProcessor region evaluation, SubmitLocation) - Drop fetch coalescing in ConnectivityWrapper: joining an in-flight fetch could return state older than the event that triggered the refresh - watchOS: read last-known network state live from WatchUserDefaults, mirroring the Catalyst macBridge live read - Restore synchronous handled/unhandled decision for the macOS status item browser-open action (and the browser-launch scene path) via a new Server.activeURLUsingLastKnownNetworkState(), so a missing active URL falls through to the toggle behavior again - Assist: deliver the TTS media URL synchronously again (evaluated against cached state) to preserve delegate ordering and threading - ConnectionInfo tests: restore the global connectivity overrides in tearDown to avoid cross-test pollution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
Contributor
There was a problem hiding this comment.
Pull request overview
This PR modernizes the app’s network/URL-selection surface to be async-first by removing deprecated synchronous URL evaluation and ensuring SSID/BSSID/hardware-address dependent decisions refresh against current network state before selecting internal/external/remote URLs.
Changes:
- Introduces
NetworkStateand rewritesConnectivityWrapperaround async network-state fetching + a synchronouslastKnownNetworkState()escape hatch. - Migrates
ConnectionInfo/ServerURL accessors (activeURL,activeAPIURL,webhookURL, internal-network detection) to async and updates many call sites (WebView, CarPlay, notifications/camera, onboarding/auth, widgets, watch). - Updates PromiseKit-heavy paths with Swift Concurrency bridges and rewrites/updates tests to async expectations.
Reviewed changes
Copilot reviewed 69 out of 69 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/Shared/Webhook/WebhookManager.test.swift | Adjusts tests to use synchronous webhook evaluation helper. |
| Tests/Shared/Sensors/ConnectivitySensor.test.swift | Updates sensor tests to stub currentNetworkState. |
| Tests/Shared/ConnectionInfo.test.swift | Rewrites ConnectionInfo tests to async and NetworkState-driven behavior. |
| Tests/App/ZoneManager/ZoneManagerProcessor.test.swift | Updates zone processor tests to new SSID fetching API. |
| Tests/App/ZoneManager/ZoneManager.test.swift | Updates zone manager tests to new SSID fetching API. |
| Tests/App/Auth/OnboardingAuth.test.swift | Updates onboarding auth tests for async token exchange + network state. |
| Sources/Watch/WatchCommunicatorService.swift | Switches watch-context sync calls to the new fire-and-forget wrapper. |
| Sources/Shared/MagicItem/MagicItem.swift | Makes watch execution path use async server.activeURL(). |
| Sources/Shared/Intents/GetCameraImageIntentHandler.swift | Switches to cached active URL evaluation in error string. |
| Sources/Shared/Intents/AppIntent/AssistInApp/AssistService.swift | Uses last-known-state URL evaluation for synchronous TTS media URL emission. |
| Sources/Shared/Environment/Environment.swift | Updates sync “has usable URL” guard to cached evaluation. |
| Sources/Shared/Environment/ConnectivityWrapper.swift | Adds NetworkState, async fetch/refresh, and cached last-known snapshot semantics. |
| Sources/Shared/Environment/AppDatabaseUpdater.swift | Makes server update decision await server.activeURL(). |
| Sources/Shared/Common/Extensions/Guarantee+Additions.swift | Adds Promise.asyncValue() bridge to Swift Concurrency. |
| Sources/Shared/API/Webhook/Sensors/ConnectivitySensor.swift | Fetches network state asynchronously for SSID/BSSID sensors. |
| Sources/Shared/API/Webhook/Networking/WebhookResponseUpdateComplications.swift | Switches watch-context sync calls to the new wrapper. |
| Sources/Shared/API/Webhook/Networking/WebhookManager.swift | Bridges webhook URL selection + 503 retry path to async URL APIs. |
| Sources/Shared/API/WatchHelpers.swift | Makes watch-context building and sync async; adds fire-and-forget wrapper. |
| Sources/Shared/API/Server.swift | Adds async activeAPIURL/webhookURL and sync last-known-state accessor. |
| Sources/Shared/API/Models/WebhookUpdateLocation.swift | Passes SSID in as a parameter instead of fetching synchronously. |
| Sources/Shared/API/Models/LegacyModelManager.swift | Uses cached evaluation for sync subscribe filtering. |
| Sources/Shared/API/HAAPI+RequestHelpers.swift | Bridges API request helpers to async server.activeAPIURL(). |
| Sources/Shared/API/HAAPI.swift | Uses cached evaluation for HAKit sync closure; bridges several URL-dependent operations to async. |
| Sources/Shared/API/ConnectionInfo.swift | Removes deprecated sync activeURL() and makes URL evaluation async-first with cached sync core. |
| Sources/Shared/API/Authentication/TokenManager.swift | Makes initial token exchange async throws. |
| Sources/Shared/API/Authentication/AuthenticationAPI.swift | Bridges token refresh/revoke base-URL selection to async. |
| Sources/Improv/ImprovDiscoverView.swift | Migrates SSID fetch to async. |
| Sources/Extensions/Widgets/Scene/SceneAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/Widgets/Controls/Switch/SwitchIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/Widgets/Controls/Light/LightIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/Widgets/Controls/Fan/FanIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/Widgets/Controls/Cover/CoverIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/Widgets/Controls/Button/ButtonIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/Widgets/Controls/Automation/AutomationAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/Watch/Notifications/NotificationSubControllerMJPEG.swift | Uses async activeAPIURL() for watch notification streaming path. |
| Sources/Extensions/Watch/ExtensionDelegate.swift | Switches watch-context sync calls to the new wrapper. |
| Sources/Extensions/NotificationContent/CameraViewController.swift | Retrieves base URL asynchronously once and threads it to stream controllers. |
| Sources/Extensions/NotificationContent/CameraStreamWebRTCViewController.swift | Updates initializer signature to accept precomputed base URL. |
| Sources/Extensions/NotificationContent/CameraStreamMJPEGViewController.swift | Uses injected base URL instead of fetching active URL synchronously. |
| Sources/Extensions/NotificationContent/CameraStreamHLSViewController.swift | Uses injected base URL instead of fetching active URL synchronously. |
| Sources/Extensions/NotificationContent/CameraStreamHandler.swift | Updates protocol initializer to accept base URL. |
| Sources/Extensions/AppIntents/Widget/Script/WidgetScriptsAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/AppIntents/UpdateSensorsAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/AppIntents/UpdateLocationAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/AppIntents/Script/ScriptAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/AppIntents/RenderTemplateAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/AppIntents/PerformActionAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/AppIntents/GetCameraSnapshotAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/Extensions/AppIntents/AssistPromptAppIntent.swift | Replaces sync network refresh with async refresh call. |
| Sources/CarPlay/Templates/Servers/CarPlayServerListTemplate.swift | Makes server list filtering await activeURL() and updates UI refresh flow. |
| Sources/App/ZoneManager/ZoneManagerProcessor.swift | Fetches SSID asynchronously while preserving Realm confinement guarantees. |
| Sources/App/ZoneManager/ZoneManager.swift | Fetches SSID asynchronously for logging while starting background task early. |
| Sources/App/Utilities/MenuManager.swift | Uses last-known-state sync URL evaluation for macOS menu open behavior. |
| Sources/App/Settings/DebugView.swift | Bridges watch-context sync and active URL logging to async. |
| Sources/App/Settings/Connection/ConnectionURLViewModel.swift | Makes SSID/hardware-address insertion async. |
| Sources/App/Settings/Connection/ConnectionSettingsViewModel.swift | Makes local-push retry eligibility computation async; activates server URL async on mac. |
| Sources/App/Scenes/QuickActionWindowSceneDelegate.swift | Uses last-known-state sync URL evaluation for macOS “open in browser” quick action. |
| Sources/App/Onboarding/Steps/Permissions/Steps/LocalAccessAndNetworkInput/NetworkInput/HomeNetworkInputView.swift | Loads SSID/hardware-address via async currentNetworkState(). |
| Sources/App/Onboarding/API/OnboardingAuthTokenExchange.swift | Converts token exchange protocol to async throws. |
| Sources/App/Onboarding/API/OnboardingAuth.swift | Fetches SSID asynchronously during onboarding connection setup. |
| Sources/App/Notifications/NotificationManagerLocalPushInterfaceExtension.swift | Makes retry eligibility + diagnostics gather SSID asynchronously. |
| Sources/App/LifecycleManager.swift | Replaces sync SSID priming with async refresh before first connect and on lifecycle events. |
| Sources/App/Frontend/WebView/WebViewController+WebKitDelegates.swift | Updates error-restore navigation path to use async server.webviewURL(). |
| Sources/App/Frontend/WebView/WebViewController+URLLoading.swift | Migrates active URL loading + kiosk URL selection to async server.webviewURL(). |
| Sources/App/Frontend/WebView/WebViewController+ProtocolConformance.swift | Migrates navigation/refresh paths to async URL evaluation. |
| Sources/App/Frontend/WebView/ConnectionSecurityLevelBlock/ConnectionSecurityLevelBlockViewModel.swift | Makes internal-network requirement evaluation await fresh network state. |
| Sources/App/Frontend/Extensions/ConnectionInfo+WebView.swift | Moves webview URL helpers from ConnectionInfo to async Server APIs. |
| Sources/App/Container/AppContainerCoordinator.swift | Converts open-url path to async webview URL resolution. |
| Sources/App/Cameras/CameraPlayer/CameraMJPEGPlayerView.swift | Fetches base URL asynchronously before starting MJPEG stream. |
Matches the other completion branches, which run on HAKit's main callback queue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
The error path read the last-known network state, which diverges from the success path and from the pre-migration behavior of using the SSID fetched for the event. Fixes ZoneManagerTests.testCollectorCollectsEventAndProcessorErrors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4953 +/- ##
=======================================
Coverage ? 52.02%
=======================================
Files ? 298
Lines ? 19602
Branches ? 0
=======================================
Hits ? 10197
Misses ? 9405
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Resolves the ConnectivityWrapper conflict with #4952, which removed the phone-synced SSID from the watch: the watch now reports an empty NetworkState, and the new WatchHomeViewModel.fetchNetworkInfo() awaits the async currentWiFiSSID(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
…ale VM updates, main-actor SwiftUI state writes - ServerRequestAdapter evaluates against the just-refreshed cached network state synchronously instead of spawning a Task (and a second network information fetch) per outgoing request; keeps the single-completion fix - ConnectionSettingsViewModel cancels the previous canRetryLocalPush update so an older SSID fetch resuming late can't overwrite a newer result - SwiftUI struct Tasks that write @State (Improv SSID, home network input, watch context debug button) are now explicitly @mainactor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
…-activeurl-ij1cun # Conflicts: # Sources/Shared/API/HAAPI.swift
…erver URLs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bgoncal
added a commit
that referenced
this pull request
Jul 9, 2026
…t connection on launch (#5017) ## Summary Since URL resolution moved to `async` (#4942 / #4953), every path that picks the active URL awaits a fresh `NEHotspotNetwork.fetchCurrent`. That completion handler occasionally never fires (seen at launch/foreground, before location services are ready), and there was no timeout, so `refreshNetworkInformation()` / `activeURL()` would hang indefinitely. Everything awaiting them then stalls at once: - the web view load hangs with `loadActiveURLIfNeededInProgress` stuck `true`, so no later event retries it — blank page that never reloads - the WebSocket never connects (the cold-connect `Task` never reaches `connectAPI`) - the account row shows no user (`currentUser` never completes) This matches the intermittent reports of a blank web view that won't reload, servers listed with no username, and no WebSocket connection. Fix: bound `performNetworkStateFetch()` with a 3s timeout. If the fetch doesn't return in time, keep the last-known network state instead of hanging (or overwriting it with an empty one). Callers always make progress, and the normal launch/foreground events re-drive the connection once network info is available. Catalyst reads network info synchronously and is unaffected. ## Screenshots Not applicable — no user-facing/UI change. ## Link to pull request in Documentation repository Documentation: home-assistant/companion.home-assistant# ## Any other notes - 3s is a safety cap only; `fetchCurrent` normally returns well under a second. - On timeout the previous value is preserved rather than cleared, so a slow fetch can't flip the app to the wrong URL. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Network information (SSID, BSSID, hardware address) is only available asynchronously on Apple platforms, so this PR removes the deprecated synchronous
ConnectionInfo.activeURL()and makes the whole network-information surface async-first, ensuring URL selection always evaluates against freshly fetched network state instead of a possibly stale cache.Core changes
ConnectivityWrapperrewritten around a newNetworkStatesnapshot (ssid,bssid,hardwareAddress):currentNetworkState(),currentWiFiSSID(),currentWiFiBSSID(),currentNetworkHardwareAddress()are async and always perform a fresh fetch (no coalescing onto in-flight fetches — a joined fetch could return state older than the event that triggered the call).lastKnownNetworkState()is the single synchronous escape hatch for consumers that cannot await; on Catalyst it reads macBridge live (matching previous always-current behavior). The watch reports an empty state since Prefer direct Watch-to-HA execution for magic items, fall back to iPhone #4952 removed the phone-synced SSID.syncNetworkInformationis gone; everything funnels throughrefreshNetworkInformation().ConnectionInfo: the deprecated syncactiveURL()is removed;activeAPIURL(),webhookURL()andisOnInternalNetwork()are now async and refresh network info before evaluating. The synchronous evaluation core stays internal (evaluateActiveURL()/evaluateWebhookURL()).Servergains asyncactiveAPIURL()/webhookURL()siblings of the existing asyncactiveURL(), with proper write-back of the re-evaluatedactiveURLType, plus a syncactiveURLUsingLastKnownNetworkState()for the few synchronous decision points (macOS status item, Assist TTS ordering).connectionInfoclosure (HAKit invokes it synchronously on every (re)connect); it evaluates against the last-known state, which is refreshed on connectivity changes and app lifecycle events. Since we control our usage of HAKit, we can revisit this if HAKit grows an async connection-info hook.Call-site migration
Taskinside the promise (HAAPIrequest helpers,DownloadDataAt,getCameraSnapshot,AuthenticationAPI,WebhookManagerURL build + 503 retry). A sharedPromise.asyncValue()bridge was added.TokenManager.initialTokenand the onboarding token exchange are nowasync throws.SyncWatchContext()is now async with a fire-and-forgetsyncWatchContext()wrapper), location updates (WebhookUpdateLocationtakes the SSID as a parameter), connection settings/URL settings view models, onboarding, Improv.ServerRequestAdapterrefreshes network info before adapting each request, and no longer calls its completion twice when no active URL is available (pre-existing bug).Behavior notes
Current.api(for:)keeps a synchronous "has any usable URL" guard against cached state, since it's called from many synchronous paths and only nil-checks.Tests
ConnectionInfotests rewritten as async tests driven byNetworkStateoverrides (restoring the global overrides intearDown), including new coverage that the async accessors refresh before evaluating and thatisOnInternalNetwork()fetches fresh state.Screenshots
Not applicable — no user-facing UI changes.
Link to pull request in Documentation repository
Documentation: home-assistant/companion.home-assistant#
Any other notes
The plan and rationale for this migration were discussed in a Claude session; the phases (connectivity wrapper → ConnectionInfo/Server API → call sites → PromiseKit paths → SSID consumers → tests) were implemented together here since they are interdependent once the sync API is removed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UEokBwHQ2cir3q4WEjm6Du
Generated by Claude Code