Skip to content

Remove deprecated sync activeURL() and make SSID/hardware-address access async#4953

Merged
bgoncal merged 11 commits into
mainfrom
claude/deprecate-sync-activeurl-ij1cun
Jul 8, 2026
Merged

Remove deprecated sync activeURL() and make SSID/hardware-address access async#4953
bgoncal merged 11 commits into
mainfrom
claude/deprecate-sync-activeurl-ij1cun

Conversation

@bgoncal

@bgoncal bgoncal commented Jul 6, 2026

Copy link
Copy Markdown
Member

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

  • ConnectivityWrapper rewritten around a new NetworkState snapshot (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.
    • The completion-handler syncNetworkInformation is gone; everything funnels through refreshNetworkInformation().
  • ConnectionInfo: the deprecated sync activeURL() is removed; activeAPIURL(), webhookURL() and isOnInternalNetwork() are now async and refresh network info before evaluating. The synchronous evaluation core stays internal (evaluateActiveURL()/evaluateWebhookURL()).
  • Server gains async activeAPIURL()/webhookURL() siblings of the existing async activeURL(), with proper write-back of the re-evaluated activeURLType, plus a sync activeURLUsingLastKnownNetworkState() for the few synchronous decision points (macOS status item, Assist TTS ordering).
  • The one intentionally-sync consumer left is HAKit's connectionInfo closure (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

  • WebView URL loading/refresh/kiosk paths, camera streaming (in-app, notification content extension, watch), CarPlay server list, quick actions, Mac menu/browser opening, Debug view, Assist TTS, MagicItem watch execution.
  • PromiseKit-entangled paths bridge via Task inside the promise (HAAPI request helpers, DownloadDataAt, getCameraSnapshot, AuthenticationAPI, WebhookManager URL build + 503 retry). A shared Promise.asyncValue() bridge was added.
  • TokenManager.initialToken and the onboarding token exchange are now async throws.
  • SSID consumers now fetch async: zone manager (SSID filter + diagnostics), local push retry diagnostics, connectivity sensors, watch context sync (SyncWatchContext() is now async with a fire-and-forget syncWatchContext() wrapper), location updates (WebhookUpdateLocation takes the SSID as a parameter), connection settings/URL settings view models, onboarding, Improv.
  • ServerRequestAdapter refreshes network info before adapting each request, and no longer calls its completion twice when no active URL is available (pre-existing bug).

Behavior notes

  • Realm thread-confinement is preserved in the zone manager / location submission paths by hopping back to the main queue (PromiseKit's default) after the async SSID fetch, and Realm zone objects are guarded against invalidation across the new suspension points.
  • The zone manager still starts its background task before any async work so location events aren't lost to suspension.
  • 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

  • ConnectionInfo tests rewritten as async tests driven by NetworkState overrides (restoring the global overrides in tearDown), including new coverage that the async accessors refresh before evaluating and that isOnInternalNetwork() fetches fresh state.
  • WebhookManager, ConnectivitySensor, ZoneManager, and OnboardingAuth tests updated to the new API.

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

claude added 4 commits July 6, 2026 15:34
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
- 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 NetworkState and rewrites ConnectivityWrapper around async network-state fetching + a synchronous lastKnownNetworkState() escape hatch.
  • Migrates ConnectionInfo / Server URL 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.

Comment thread Sources/App/Frontend/WebView/WebViewController+URLLoading.swift
Comment thread Sources/App/Frontend/WebView/WebViewController+URLLoading.swift
Comment thread Sources/App/Container/AppContainerCoordinator.swift
Comment thread Sources/CarPlay/Templates/Servers/CarPlayServerListTemplate.swift
Comment thread Sources/CarPlay/Templates/Servers/CarPlayServerListTemplate.swift
Comment thread Sources/Shared/Environment/ConnectivityWrapper.swift
Comment thread Sources/Shared/API/HAAPI.swift Outdated
claude added 2 commits July 6, 2026 16:02
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

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.53659% with 211 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@ae3d6af). Learn more about missing BASE report.

Files with missing lines Patch % Lines
Sources/Shared/API/HAAPI.swift 22.60% 89 Missing ⚠️
Sources/Shared/API/HAAPI+RequestHelpers.swift 0.00% 65 Missing ⚠️
.../Shared/API/Authentication/AuthenticationAPI.swift 33.33% 20 Missing ⚠️
...Shared/API/Webhook/Networking/WebhookManager.swift 68.88% 14 Missing ⚠️
...urces/Shared/API/Authentication/TokenManager.swift 0.00% 9 Missing ⚠️
.../Intents/AppIntent/AssistInApp/AssistService.swift 0.00% 6 Missing ⚠️
Sources/Shared/API/WatchHelpers.swift 57.14% 3 Missing ⚠️
...ources/Shared/Environment/AppDatabaseUpdater.swift 0.00% 3 Missing ⚠️
Sources/Shared/API/ConnectionInfo.swift 95.23% 1 Missing ⚠️
...s/Shared/Intents/GetCameraImageIntentHandler.swift 0.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bgoncal bgoncal marked this pull request as ready for review July 6, 2026 20:13
claude and others added 3 commits July 6, 2026 20:16
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
@bgoncal bgoncal merged commit b3a4bff into main Jul 8, 2026
17 checks passed
@bgoncal bgoncal deleted the claude/deprecate-sync-activeurl-ij1cun branch July 8, 2026 07:54
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants