Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ struct HomeNetworkInputView: View {
}

private func loadCurrentNetworkInfo() {
Current.connectivity.syncNetworkInformation {
networkName = Current.connectivity.currentWiFiSSID() ?? ""
Task { @MainActor in
let networkInfo = await Current.connectivity.currentNetworkInfo()
networkName = networkInfo.ssid ?? ""
hardwareAddress = Current.connectivity.currentNetworkHardwareAddress() ?? ""
}
}
Expand Down
54 changes: 54 additions & 0 deletions Sources/App/WebView/Extensions/ConnectionInfo+WebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,35 @@ extension ConnectionInfo {
return components
}

/// Async version that fetches real-time network info.
mutating func webviewURLComponents() async -> URLComponents? {
if Current.appConfiguration == .fastlaneSnapshot, prefs.object(forKey: "useDemo") != nil {
return URLComponents(string: "https://companion.home-assistant.io/app/ios/demo")!
}
guard let activeURL = await activeURL() else {
Current.Log.error("No activeURL available while webviewURLComponents was called")
return nil
}

guard var components = URLComponents(url: activeURL, resolvingAgainstBaseURL: true) else {
return nil
}

let queryItem = URLQueryItem(name: "external_auth", value: "1")
components.queryItems = [queryItem]

return components
}

mutating func webviewURL() -> URL? {
webviewURLComponents()?.url
}

/// Async version that fetches real-time network info.
mutating func webviewURL() async -> URL? {
await webviewURLComponents()?.url
}

mutating func webviewURL(from raw: String) -> URL? {
guard let baseURLComponents = webviewURLComponents(), let baseURL = baseURLComponents.url else {
return nil
Expand Down Expand Up @@ -52,4 +77,33 @@ extension ConnectionInfo {
return nil
}
}

/// Async version that fetches real-time network info.
mutating func webviewURL(from raw: String) async -> URL? {
guard let baseURLComponents = await webviewURLComponents(), let baseURL = baseURLComponents.url else {
return nil
}

if raw.starts(with: "/") {
if let rawComponents = URLComponents(string: raw) {
var components = baseURLComponents
components.path.append(rawComponents.path)
components.fragment = rawComponents.fragment

if let items = rawComponents.queryItems {
var queryItems = components.queryItems ?? []
queryItems.append(contentsOf: items)
components.queryItems = queryItems
}

return components.url
} else {
return baseURL.appendingPathComponent(raw)
}
} else if let url = URL(string: raw), url.baseIsEqual(to: baseURL) {
return url
} else {
return nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,28 @@ final class ConnectionSecurityLevelBlockViewModel: ObservableObject {
}

func loadRequirements() {
requirements = []

// Check if home network is defined
if server.info.connection.internalSSIDs?.isEmpty ?? true,
server.info.connection.internalHardwareAddresses?.isEmpty ?? true {
requirements.append(.homeNetworkMissing)
} else {
// Check if user is on home network
if !server.info.connection.isOnInternalNetwork {
requirements.append(.notOnHomeNetwork)
Task { @MainActor in
var newRequirements: [Requirement] = []

// Check if home network is defined
if server.info.connection.internalSSIDs?.isEmpty ?? true,
server.info.connection.internalHardwareAddresses?.isEmpty ?? true {
newRequirements.append(.homeNetworkMissing)
} else {
// Check if user is on home network (fetch real-time network info)
let isOnInternal = await server.info.connection.isOnInternalNetwork()
if !isOnInternal {
newRequirements.append(.notOnHomeNetwork)
}
}

// Check location permission
let currentPermission = Current.locationManager.currentPermissionState
if currentPermission != .authorizedAlways, currentPermission != .authorizedWhenInUse {
newRequirements.append(.locationPermission)
}
}

// Check location permission
let currentPermission = Current.locationManager.currentPermissionState
if currentPermission != .authorizedAlways, currentPermission != .authorizedWhenInUse {
requirements.append(.locationPermission)
requirements = newRequirements
}
}
}
8 changes: 4 additions & 4 deletions Sources/App/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -789,13 +789,13 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
loadActiveURLIfNeededInProgress = true
Current.Log.info("loadActiveURLIfNeeded called")

Current.connectivity.syncNetworkInformation { [weak self] in
Task { @MainActor [weak self] in
defer {
self?.loadActiveURLIfNeededInProgress = false
}

guard let self else { return }
guard let webviewURL = server.info.connection.webviewURL() else {
guard let webviewURL = await server.info.connection.webviewURL() else {
Current.Log.info("not loading, no url")
showNoActiveURLError()
return
Expand Down Expand Up @@ -1534,10 +1534,10 @@ extension WebViewController: WebViewControllerProtocol {
}

@objc func refresh() {
Current.connectivity.syncNetworkInformation { [weak self] in
Task { @MainActor [weak self] in
guard let self else { return }
// called via menu/keyboard shortcut too
if let webviewURL = server.info.connection.webviewURL() {
if let webviewURL = await server.info.connection.webviewURL() {
if webView.url?.baseIsEqual(to: webviewURL) == true, !lastNavigationWasServerError {
reload()
} else {
Expand Down
7 changes: 4 additions & 3 deletions Sources/Improv/ImprovDiscoverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,10 @@ struct ImprovDiscoverView<Manager>: View where Manager: ImprovManagerProtocol {
selectedPeripheral = peripheral

// This only works if location permission is permitted
Current.connectivity.syncNetworkInformation {
if let ssid = Current.connectivity.currentWiFiSSID() {
self.ssid = ssid
Task { @MainActor in
let networkInfo = await Current.connectivity.currentNetworkInfo()
if let networkSSID = networkInfo.ssid {
ssid = networkSSID
}
}
showWifiAlert = true
Expand Down
103 changes: 101 additions & 2 deletions Sources/Shared/API/ConnectionInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,72 @@ public struct ConnectionInfo: Codable, Equatable {
activeURLType = .external
url = externalURL
} else if let internalURL, [.lessSecure, .undefined].contains(connectionAccessSecurityLevel) {
// Falback to internal URL if no other URL is set
// Fallback to internal URL if no other URL is set
// In case user opted to not check for home network or haven't made a decision yet
// we allow usage of internal URL as fallback
activeURLType = .internal
url = internalURL
} else if let internalURL, internalURL.scheme == "https" {
// Falback to internal URL if no other URL is set and internal URL is HTTPS
// Fallback to internal URL if no other URL is set and internal URL is HTTPS
activeURLType = .internal
url = internalURL
} else {
url = nil
activeURLType = .none
}

return url?.sanitized()
}

/// Returns the url that should be used at this moment to access the Home Assistant instance.
/// This version fetches real-time network information asynchronously, avoiding race conditions.
public mutating func activeURL() async -> URL? {
if let overrideActiveURLType {
let overrideURL: URL?

switch overrideActiveURLType {
case .internal:
activeURLType = .internal
overrideURL = internalURL
case .remoteUI:
activeURLType = .remoteUI
overrideURL = remoteUIURL
case .external:
activeURLType = .external
overrideURL = externalURL
case .none:
activeURLType = .none
overrideURL = nil
}

if let overrideURL {
return overrideURL.sanitized()
}
}

let url: URL?
let onInternalNetwork = await isOnInternalNetwork()

if let internalURL, onInternalNetwork || overrideActiveURLType == .internal {
// Home network, local connection
activeURLType = .internal
url = internalURL
} else if let remoteUIURL, useCloud {
// Home Assistant Cloud connection
activeURLType = .remoteUI
url = remoteUIURL
} else if let externalURL {
// Custom remote connection
activeURLType = .external
url = externalURL
} else if let internalURL, [.lessSecure, .undefined].contains(connectionAccessSecurityLevel) {
// Fallback to internal URL if no other URL is set
// In case user opted to not check for home network or haven't made a decision yet
// we allow usage of internal URL as fallback
activeURLType = .internal
url = internalURL
} else if let internalURL, internalURL.scheme == "https" {
// Fallback to internal URL if no other URL is set and internal URL is HTTPS
activeURLType = .internal
url = internalURL
} else {
Expand Down Expand Up @@ -282,6 +341,15 @@ public struct ConnectionInfo: Codable, Equatable {
}
}

/// Returns the activeURL with /api appended. Fetches real-time network info.
public mutating func activeAPIURL() async -> URL? {
if let activeURL = await activeURL() {
return activeURL.appendingPathComponent("api", isDirectory: false)
} else {
return nil
}
}

public mutating func webhookURL() -> URL? {
if let cloudhookURL, !isOnInternalNetwork {
return cloudhookURL
Expand All @@ -294,6 +362,20 @@ public struct ConnectionInfo: Codable, Equatable {
}
}

/// Returns the webhook URL. Fetches real-time network info.
public mutating func webhookURL() async -> URL? {
let onInternalNetwork = await isOnInternalNetwork()
if let cloudhookURL, !onInternalNetwork {
return cloudhookURL
}

if let activeURL = await activeURL() {
return activeURL.appendingPathComponent(webhookPath, isDirectory: false)
} else {
return nil
}
}

public var webhookPath: String {
"api/webhook/\(webhookID)"
}
Expand Down Expand Up @@ -321,6 +403,7 @@ public struct ConnectionInfo: Codable, Equatable {
}

/// Returns true if current SSID is SSID marked for internal URL use.
/// Note: This uses cached network info. For real-time network state, use `isOnInternalNetwork() async`.
public var isOnInternalNetwork: Bool {
if let current = Current.connectivity.currentWiFiSSID(),
internalSSIDs?.contains(current) == true {
Expand All @@ -335,6 +418,22 @@ public struct ConnectionInfo: Codable, Equatable {
return false
}

/// Returns true if current SSID is SSID marked for internal URL use.
/// This fetches real-time network information asynchronously.
public func isOnInternalNetwork() async -> Bool {
let networkInfo = await Current.connectivity.currentNetworkInfo()
if let ssid = networkInfo.ssid, internalSSIDs?.contains(ssid) == true {
return true
}

if let current = Current.connectivity.currentNetworkHardwareAddress(),
internalHardwareAddresses?.contains(current) == true {
return true
}

return false
}

public var hasInternalURLSet: Bool {
internalURL != nil
}
Expand Down
Loading
Loading