Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors Tracker to include OH version #834

Merged
merged 2 commits into from
Sep 20, 2024
Merged
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
104 changes: 55 additions & 49 deletions OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ public enum NetworkStatus: String {
case connected = "Connected"
}

// Anticipating supporting more robust configuration options where we allow multiple url/user/pass options for users
public struct ConnectionObject: Equatable {
public struct ConnectionConfiguration: Equatable {
public let url: String
public let priority: Int // Lower is higher priority, 0 is primary

Expand All @@ -32,15 +31,20 @@ public struct ConnectionObject: Equatable {
}
}

public struct ConnectionInfo: Equatable {
public let configuration: ConnectionConfiguration
public let version: Int
}

public final class NetworkTracker: ObservableObject {
public static let shared = NetworkTracker()

@Published public private(set) var activeServer: ConnectionObject?
@Published public private(set) var activeConnection: ConnectionInfo?
@Published public private(set) var status: NetworkStatus = .connecting

private let monitor: NWPathMonitor
private let monitorQueue = DispatchQueue.global(qos: .background)
private var connectionObjects: [ConnectionObject] = []
private var connectionConfigurations: [ConnectionConfiguration] = []

private var retryTimer: DispatchSourceTimer?
private let timerQueue = DispatchQueue(label: "com.openhab.networktracker.timerQueue")
Expand All @@ -53,50 +57,51 @@ public final class NetworkTracker: ObservableObject {
monitor.pathUpdateHandler = { [weak self] path in
if path.status == .satisfied {
os_log("Network status: Connected", log: OSLog.default, type: .info)
self?.checkActiveServer()
self?.checkActiveConnection()
} else {
os_log("Network status: Disconnected", log: OSLog.default, type: .info)
self?.setActiveServer(nil)
self?.setActiveConnection(nil)
self?.startRetryTimer(10) // try every 10 seconds connect
}
}
monitor.start(queue: monitorQueue)
}

public func startTracking(connectionObjects: [ConnectionObject]) {
self.connectionObjects = connectionObjects
public func startTracking(connectionConfigurations: [ConnectionConfiguration]) {
self.connectionConfigurations = connectionConfigurations
attemptConnection()
}

// This gets called periodically when we have an active server to make sure its still the best choice
private func checkActiveServer() {
guard let activeServer, activeServer.priority == 0 else {
// No primary active server, proceed with the normal connection attempt
// This gets called periodically when we have an active connection to make sure it's still the best choice
private func checkActiveConnection() {
guard let activeConnection else {
// No active connection, proceed with the normal connection attempt
attemptConnection()
return
}
// Check if the primary (priority = 0) active server is reachable if thats what is currenty connected.
NetworkConnection.tracker(openHABRootUrl: activeServer.url) { [weak self] response in

// Check if the active connection is reachable
NetworkConnection.tracker(openHABRootUrl: activeConnection.configuration.url) { [weak self] response in
switch response.result {
case .success:
os_log("Network status: Active server is reachable: %{PUBLIC}@", log: OSLog.default, type: .info, activeServer.url)
os_log("Network status: Active connection is reachable: %{PUBLIC}@", log: OSLog.default, type: .info, activeConnection.configuration.url)
case .failure:
os_log("Network status: Active server is not reachable: %{PUBLIC}@", log: OSLog.default, type: .error, activeServer.url)
os_log("Network status: Active connection is not reachable: %{PUBLIC}@", log: OSLog.default, type: .error, activeConnection.configuration.url)
self?.attemptConnection() // If not reachable, run the connection logic
}
}
}

private func attemptConnection() {
guard !connectionObjects.isEmpty else {
os_log("Network status: No connection objects available.", log: OSLog.default, type: .error)
setActiveServer(nil)
guard !connectionConfigurations.isEmpty else {
os_log("Network status: No connection configurations available.", log: OSLog.default, type: .error)
setActiveConnection(nil)
return
}
os_log("Network status: checking available servers....", log: OSLog.default, type: .error)
os_log("Network status: Checking available connections....", log: OSLog.default, type: .info)
let dispatchGroup = DispatchGroup()
var highestPriorityConnection: ConnectionObject?
var firstAvailableConnection: ConnectionObject?
var highestPriorityConnection: ConnectionInfo?
var firstAvailableConnection: ConnectionInfo?
var checkOutstanding = false // Track if there are any checks still in progress

let priorityWaitTime: TimeInterval = 2.0
Expand All @@ -107,21 +112,20 @@ public final class NetworkTracker: ObservableObject {
guard let self else { return }
// After 2 seconds, if no high-priority connection was found, check for first available connection
if let firstAvailableConnection, highestPriorityConnection == nil {
setActiveServer(firstAvailableConnection)
setActiveConnection(firstAvailableConnection)
} else if highestPriorityConnection == nil, checkOutstanding {
os_log("Network status: No server responded in 2 seconds, waiting for checks to finish.", log: OSLog.default, type: .info)
os_log("Network status: No connection responded in 2 seconds, waiting for checks to finish.", log: OSLog.default, type: .info)
} else {
os_log("Network status: No server responded in 2 seconds and no checks are outstanding.", log: OSLog.default, type: .error)
setActiveServer(nil)
os_log("Network status: No connection responded in 2 seconds and no checks are outstanding.", log: OSLog.default, type: .error)
setActiveConnection(nil)
}
}

// Begin checking each connection object in parallel
for connection in connectionObjects {
// Begin checking each connection configuration in parallel
for configuration in connectionConfigurations {
dispatchGroup.enter()
checkOutstanding = true // Signal that checks are outstanding

NetworkConnection.tracker(openHABRootUrl: connection.url) { [weak self] response in
NetworkConnection.tracker(openHABRootUrl: configuration.url) { [weak self] response in
guard let self else { return }
defer {
dispatchGroup.leave() // When each check completes, this signals the group that it's done
Expand All @@ -131,23 +135,25 @@ public final class NetworkTracker: ObservableObject {
case let .success(data):
let version = getServerInfoFromData(data: data)
if version > 0 {
if connection.priority == 0, highestPriorityConnection == nil {
// Found a high-priority (0) connection
highestPriorityConnection = connection
let connectionInfo = ConnectionInfo(configuration: configuration, version: version)
if configuration.priority == 0, highestPriorityConnection == nil {
// Found a high-priority (0) connection
highestPriorityConnection = connectionInfo
priorityWorkItem?.cancel() // Stop the 2-second wait if highest priority succeeds
setActiveServer(connection)
setActiveConnection(connectionInfo)
} else if highestPriorityConnection == nil {
// Check if this connection has a higher priority than the current firstAvailableConnection
if firstAvailableConnection == nil || connection.priority < firstAvailableConnection!.priority {
os_log("Network status: Found a higher priority available connection: %{PUBLIC}@", log: OSLog.default, type: .info, connection.url)
firstAvailableConnection = connection
let connectionInfo = ConnectionInfo(configuration: configuration, version: version)
if firstAvailableConnection == nil || configuration.priority < firstAvailableConnection!.configuration.priority {
os_log("Network status: Found a higher priority available connection: %{PUBLIC}@", log: OSLog.default, type: .info, configuration.url)
firstAvailableConnection = connectionInfo
}
}
} else {
os_log("Network status: Invalid server version from %{PUBLIC}@", log: OSLog.default, type: .error, connection.url)
os_log("Network status: Invalid server version from %{PUBLIC}@", log: OSLog.default, type: .error, configuration.url)
}
case let .failure(error):
os_log("Network status: Failed to connect to %{PUBLIC}@ : %{PUBLIC}@", log: OSLog.default, type: .error, connection.url, error.localizedDescription)
os_log("Network status: Failed to connect to %{PUBLIC}@ : %{PUBLIC}@", log: OSLog.default, type: .error, configuration.url, error.localizedDescription)
}
}
}
Expand All @@ -164,17 +170,17 @@ public final class NetworkTracker: ObservableObject {

// If a high-priority connection was already established, we are done
if let highestPriorityConnection {
os_log("Network status: High-priority connection established with %{PUBLIC}@", log: OSLog.default, type: .info, highestPriorityConnection.url)
os_log("Network status: High-priority connection established with %{PUBLIC}@", log: OSLog.default, type: .info, highestPriorityConnection.configuration.url)
return
}

// If we have an available connection and no high-priority connection, set the first available
if let firstAvailableConnection {
setActiveServer(firstAvailableConnection)
os_log("Network status: First available connection established with %{PUBLIC}@", log: OSLog.default, type: .info, firstAvailableConnection.url)
setActiveConnection(firstAvailableConnection)
os_log("Network status: First available connection established with %{PUBLIC}@", log: OSLog.default, type: .info, firstAvailableConnection.configuration.url)
} else {
os_log("Network status: No server responded, connection failed.", log: OSLog.default, type: .error)
setActiveServer(nil)
os_log("Network status: No connection responded, connection failed.", log: OSLog.default, type: .error)
setActiveConnection(nil)
}
}
}
Expand All @@ -200,11 +206,11 @@ public final class NetworkTracker: ObservableObject {
}
}

private func setActiveServer(_ server: ConnectionObject?) {
os_log("Network status: setActiveServer: %{PUBLIC}@", log: OSLog.default, type: .info, server?.url ?? "no server")
guard activeServer != server else { return }
activeServer = server
if activeServer != nil {
private func setActiveConnection(_ connection: ConnectionInfo?) {
os_log("Network status: setActiveConnection: %{PUBLIC}@", log: OSLog.default, type: .info, connection?.configuration.url ?? "no connection")
guard activeConnection != connection else { return }
activeConnection = connection
if activeConnection != nil {
updateStatus(.connected)
startRetryTimer(connectedRetryInterval)
} else {
Expand Down
18 changes: 9 additions & 9 deletions OpenHABCore/Sources/OpenHABCore/Util/OpenHABItemCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ public class OpenHABItemCache {
if NetworkConnection.shared == nil {
NetworkConnection.initialize(ignoreSSL: Preferences.ignoreSSL, interceptor: nil)
}
let connection1 = ConnectionObject(
let connection1 = ConnectionConfiguration(
url: Preferences.localUrl,
priority: 0
)
let connection2 = ConnectionObject(
let connection2 = ConnectionConfiguration(
url: Preferences.remoteUrl,
priority: 1
)
NetworkTracker.shared.startTracking(connectionObjects: [connection1, connection2])
NetworkTracker.shared.startTracking(connectionConfigurations: [connection1, connection2])
}

public func getItemNames(searchTerm: String?, types: [OpenHABItem.ItemType]?, completion: @escaping ([NSString]) -> Void) {
Expand Down Expand Up @@ -75,12 +75,12 @@ public class OpenHABItemCache {

@available(iOS 12.0, *)
public func reload(searchTerm: String?, types: [OpenHABItem.ItemType]?, completion: @escaping ([NSString]) -> Void) {
NetworkTracker.shared.$activeServer
NetworkTracker.shared.$activeConnection
.filter { $0 != nil } // Only proceed if activeServer is not nil
.first() // Automatically cancels after the first non-nil value
.receive(on: DispatchQueue.main)
.sink { activeServer in
if let urlString = activeServer?.url, let url = Endpoint.items(openHABRootUrl: urlString).url {
.sink { activeConnection in
if let urlString = activeConnection?.configuration.url, let url = Endpoint.items(openHABRootUrl: urlString).url {
os_log("OpenHABItemCache Loading items from %{PUBLIC}@", log: .default, type: .info, urlString)
self.lastLoad = Date().timeIntervalSince1970
NetworkConnection.load(from: url, timeout: self.timeout) { response in
Expand All @@ -105,12 +105,12 @@ public class OpenHABItemCache {

@available(iOS 12.0, *)
public func reload(name: String, completion: @escaping (OpenHABItem?) -> Void) {
NetworkTracker.shared.$activeServer
NetworkTracker.shared.$activeConnection
.filter { $0 != nil } // Only proceed if activeServer is not nil
.first() // Automatically cancels after the first non-nil value
.receive(on: DispatchQueue.main)
.sink { activeServer in
if let urlString = activeServer?.url, let url = Endpoint.items(openHABRootUrl: urlString).url {
.sink { activeConnection in
if let urlString = activeConnection?.configuration.url, let url = Endpoint.items(openHABRootUrl: urlString).url {
os_log("OpenHABItemCache Loading items from %{PUBLIC}@", log: .default, type: .info, urlString)
self.lastLoad = Date().timeIntervalSince1970
NetworkConnection.load(from: url, timeout: self.timeout) { response in
Expand Down
8 changes: 4 additions & 4 deletions openHAB/DrawerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,11 @@ struct DrawerView: View {
}

private func trackActiveServer() {
trackerCancellable = NetworkTracker.shared.$activeServer
trackerCancellable = NetworkTracker.shared.$activeConnection
.receive(on: DispatchQueue.main)
.sink { activeServer in
if let activeServer {
connectedUrl = activeServer.url
.sink { activeConnection in
if let activeConnection {
connectedUrl = activeConnection.configuration.url
} else {
connectedUrl = NSLocalizedString("connecting", comment: "")
}
Expand Down
31 changes: 16 additions & 15 deletions openHAB/OpenHABRootViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,31 +127,32 @@ class OpenHABRootViewController: UIViewController {
)
.sink { (localUrl, remoteUrl, demomode) in
if demomode {
NetworkTracker.shared.startTracking(connectionObjects: [
ConnectionObject(
NetworkTracker.shared.startTracking(connectionConfigurations: [
ConnectionConfiguration(
url: "https://demo.openhab.org",
priority: 0
)
])
} else {
let connection1 = ConnectionObject(
let connection1 = ConnectionConfiguration(
url: localUrl,
priority: 0
)
let connection2 = ConnectionObject(
let connection2 = ConnectionConfiguration(
url: remoteUrl,
priority: 1
)
NetworkTracker.shared.startTracking(connectionObjects: [connection1, connection2])
NetworkTracker.shared.startTracking(connectionConfigurations: [connection1, connection2])
}
}
.store(in: &cancellables)

NetworkTracker.shared.$activeServer
NetworkTracker.shared.$activeConnection
.receive(on: DispatchQueue.main)
.sink { [weak self] activeServer in
if let activeServer {
self?.appData?.openHABRootUrl = activeServer.url
.sink { [weak self] activeConnection in
if let activeConnection {
self?.appData?.openHABRootUrl = activeConnection.configuration.url
self?.appData?.openHABVersion = activeConnection.version
}
}
.store(in: &cancellables)
Expand Down Expand Up @@ -342,10 +343,10 @@ class OpenHABRootViewController: UIViewController {
let itemName = String(components[0])
let itemCommand = String(components[1])
// This will only fire onece since we do not retain the return cancelable
_ = NetworkTracker.shared.$activeServer
_ = NetworkTracker.shared.$activeConnection
.receive(on: DispatchQueue.main)
.sink { activeServer in
if let openHABUrl = activeServer?.url {
.sink { activeConnection in
if let openHABUrl = activeConnection?.configuration.url {
os_log("Sending comand", log: .default, type: .error)
let client = HTTPClient(username: Preferences.username, password: Preferences.password)
client.doPost(baseURLs: [openHABUrl], path: "/rest/items/\(itemName)", body: itemCommand) { data, _, error in
Expand Down Expand Up @@ -418,10 +419,10 @@ class OpenHABRootViewController: UIViewController {
}

// This will only fire onece since we do not retain the return cancelable
_ = NetworkTracker.shared.$activeServer
_ = NetworkTracker.shared.$activeConnection
.receive(on: DispatchQueue.main)
.sink { activeServer in
if let openHABUrl = activeServer?.url {
.sink { activeConnection in
if let openHABUrl = activeConnection?.configuration.url {
os_log("Sending comand", log: .default, type: .error)
let client = HTTPClient(username: Preferences.username, password: Preferences.password)
client.doPost(baseURLs: [openHABUrl], path: "/rest/rules/rules/\(uuid)/runnow", body: jsonString) { data, _, error in
Expand Down
Loading