Skip to content

Commit

Permalink
Tracker and openHAB Cache updates (#829)
Browse files Browse the repository at this point in the history
* Removes one of the tracker states, integrates into item cache.

Signed-off-by: Dan Cunningham <[email protected]>

* revert package.resolved

Signed-off-by: Dan Cunningham <[email protected]>

* Synchonize access to retry timer

Signed-off-by: Dan Cunningham <[email protected]>

* Clean up tracker a bit more, remove empty test, add tacker indicator to sidemenu

Signed-off-by: Dan Cunningham <[email protected]>

* Make sure to support demo mode when initing the tracker

Signed-off-by: Dan Cunningham <[email protected]>

* Trigger Build

---------

Signed-off-by: Dan Cunningham <[email protected]>
  • Loading branch information
digitaldan authored Sep 18, 2024
1 parent 0d86170 commit e0069b4
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 287 deletions.
65 changes: 35 additions & 30 deletions OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public enum NetworkStatus: String {
case notConnected = "Not Connected"
case connecting = "Connecting"
case connected = "Connected"
case connectionFailed = "Connection Failed"
}

// Anticipating supporting more robust configuration options where we allow multiple url/user/pass options for users
Expand All @@ -31,23 +30,23 @@ public struct ConnectionObject: Equatable {
self.url = url
self.priority = priority
}

public static func == (lhs: ConnectionObject, rhs: ConnectionObject) -> Bool {
lhs.url == rhs.url && lhs.priority == rhs.priority
}
}

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

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

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

private var retryTimer: DispatchSourceTimer?
private let timerQueue = DispatchQueue(label: "com.openhab.networktracker.timerQueue")

private let connectedRetryInterval: TimeInterval = 60 // amount of time we scan for better connections when connected
private let disconnectedRetryInterval: TimeInterval = 30 // amount of time we scan when not connected

private init() {
monitor = NWPathMonitor()
Expand All @@ -57,7 +56,7 @@ public final class NetworkTracker: ObservableObject {
self?.checkActiveServer()
} else {
os_log("Network status: Disconnected", log: OSLog.default, type: .info)
self?.updateStatus(.notConnected)
self?.setActiveServer(nil)
self?.startRetryTimer(10) // try every 10 seconds connect
}
}
Expand All @@ -69,19 +68,18 @@ public final class NetworkTracker: ObservableObject {
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
attemptConnection()
return
}
// Check if the last active server is reachable
// 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
switch response.result {
case .success:
os_log("Network status: Active server is reachable: %{PUBLIC}@", log: OSLog.default, type: .info, activeServer.url)
self?.updateStatus(.connected) // If reachable, we're done
self?.cancelRetryTimer()
case .failure:
os_log("Network status: Active server is not reachable: %{PUBLIC}@", log: OSLog.default, type: .error, activeServer.url)
self?.attemptConnection() // If not reachable, run the connection logic
Expand All @@ -92,7 +90,7 @@ public final class NetworkTracker: ObservableObject {
private func attemptConnection() {
guard !connectionObjects.isEmpty else {
os_log("Network status: No connection objects available.", log: OSLog.default, type: .error)
updateStatus(.notConnected)
setActiveServer(nil)
return
}
os_log("Network status: checking available servers....", log: OSLog.default, type: .error)
Expand All @@ -114,7 +112,7 @@ public final class NetworkTracker: ObservableObject {
os_log("Network status: No server 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)
updateStatus(.connectionFailed)
setActiveServer(nil)
}
}

Expand Down Expand Up @@ -176,36 +174,43 @@ public final class NetworkTracker: ObservableObject {
os_log("Network status: First available connection established with %{PUBLIC}@", log: OSLog.default, type: .info, firstAvailableConnection.url)
} else {
os_log("Network status: No server responded, connection failed.", log: OSLog.default, type: .error)
updateStatus(.connectionFailed)
setActiveServer(nil)
}
}
}

// Start the retry timer to attempt connection every N seconds
private func startRetryTimer(_ retryInterval: TimeInterval) {
cancelRetryTimer()
retryTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
retryTimer?.schedule(deadline: .now() + retryInterval, repeating: retryInterval)
retryTimer?.setEventHandler { [weak self] in
os_log("Network status: Retry timer firing", log: OSLog.default, type: .info)
self?.attemptConnection()
timerQueue.sync {
retryTimer = DispatchSource.makeTimerSource(queue: timerQueue)
retryTimer?.schedule(deadline: .now() + retryInterval, repeating: retryInterval)
retryTimer?.setEventHandler { [weak self] in
os_log("Network status: Retry timer firing", log: OSLog.default, type: .info)
self?.attemptConnection()
}
retryTimer?.resume()
}
retryTimer?.resume()
}

// Cancel the retry timer
private func cancelRetryTimer() {
retryTimer?.cancel()
retryTimer = nil
timerQueue.sync {
retryTimer?.cancel()
retryTimer = nil
}
}

private func setActiveServer(_ server: ConnectionObject) {
os_log("Network status: setActiveServer: %{PUBLIC}@", log: OSLog.default, type: .info, server.url)
if activeServer != server {
activeServer = server
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 {
updateStatus(.connected)
startRetryTimer(connectedRetryInterval)
} else {
updateStatus(.notConnected)
startRetryTimer(disconnectedRetryInterval)
}
updateStatus(.connected)
startRetryTimer(60) // check every 60 seconds to see if a better server is available.
}

private func updateStatus(_ newStatus: NetworkStatus) {
Expand Down
173 changes: 67 additions & 106 deletions OpenHABCore/Sources/OpenHABCore/Util/OpenHABItemCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,37 @@
//
// SPDX-License-Identifier: EPL-2.0

import Combine
import Foundation
import os.log

public class OpenHABItemCache {
public static let instance = OpenHABItemCache()

static let URL_NONE = 0
static let URL_LOCAL = 1
static let URL_REMOTE = 2
static let URL_DEMO = 3

public var items: [OpenHABItem]?

var timeout: Double { lastUrlConnected == OpenHABItemCache.URL_LOCAL ? 10.0 : 20.0 }
var url = ""
var localUrlFailed = false
var lastUrlConnected = URL_NONE
var cancellables = Set<AnyCancellable>()
var timeout: Double = 20
var lastLoad = Date().timeIntervalSince1970

private init() {
if NetworkConnection.shared == nil {
NetworkConnection.initialize(ignoreSSL: Preferences.ignoreSSL, interceptor: nil)
}
let connection1 = ConnectionObject(
url: Preferences.localUrl,
priority: 0
)
let connection2 = ConnectionObject(
url: Preferences.remoteUrl,
priority: 1
)
NetworkTracker.shared.startTracking(connectionObjects: [connection1, connection2])
}

public func getItemNames(searchTerm: String?, types: [OpenHABItem.ItemType]?, completion: @escaping ([NSString]) -> Void) {
var ret = [NSString]()

guard let items else {
if #available(iOS 12.0, *) {
reload(searchTerm: searchTerm, types: types, completion: completion)
} else {
// Fallback on earlier versions
}
reload(searchTerm: searchTerm, types: types, completion: completion)
return
}

Expand Down Expand Up @@ -72,114 +75,72 @@ public class OpenHABItemCache {

@available(iOS 12.0, *)
public func reload(searchTerm: String?, types: [OpenHABItem.ItemType]?, completion: @escaping ([NSString]) -> Void) {
lastLoad = Date().timeIntervalSince1970

guard let uurl = getURL() else { return }

os_log("Loading items from %{PUBLIC}@", log: .default, type: .info, url)

if NetworkConnection.shared == nil {
NetworkConnection.initialize(ignoreSSL: Preferences.ignoreSSL, interceptor: nil)
}

NetworkConnection.load(from: uurl, timeout: timeout) { response in
switch response.result {
case let .success(data):
do {
try self.decodeItemsData(data)

let ret = self.items?.filter { (searchTerm == nil || $0.name.contains(searchTerm.orEmpty)) && (types == nil || ($0.type != nil && types!.contains($0.type!))) }.sorted(by: \.name).map { NSString(string: $0.name) } ?? []

completion(ret)
} catch {
os_log("%{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
}
case let .failure(error):
if self.lastUrlConnected == OpenHABItemCache.URL_LOCAL {
self.localUrlFailed = true
os_log("%{PUBLIC}@ ", log: .default, type: .info, error.localizedDescription)
self.reload(searchTerm: searchTerm, types: types, completion: completion) // try remote

} else {
os_log("%{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
NetworkTracker.shared.$activeServer
.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 {
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
switch response.result {
case let .success(data):
do {
try self.decodeItemsData(data)
let ret = self.items?.filter { (searchTerm == nil || $0.name.contains(searchTerm.orEmpty)) && (types == nil || ($0.type != nil && types!.contains($0.type!))) }.sorted(by: \.name).map { NSString(string: $0.name) } ?? []
completion(ret)
} catch {
print(error)
os_log("OpenHABItemCache %{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
}
case let .failure(error):
os_log("OpenHABItemCache %{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
}
}
}
}
}
.store(in: &cancellables)
}

@available(iOS 12.0, *)
public func reload(name: String, completion: @escaping (OpenHABItem?) -> Void) {
lastLoad = Date().timeIntervalSince1970

guard let uurl = getURL() else { return }

os_log("Loading items from %{PUBLIC}@", log: .default, type: .info, url)

if NetworkConnection.shared == nil {
NetworkConnection.initialize(ignoreSSL: Preferences.ignoreSSL, interceptor: nil)
}

NetworkConnection.load(from: uurl, timeout: timeout) { response in
switch response.result {
case let .success(data):
do {
try self.decodeItemsData(data)

let item = self.getItem(name)

completion(item)

} catch {
os_log("%{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
NetworkTracker.shared.$activeServer
.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 {
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
switch response.result {
case let .success(data):
do {
try self.decodeItemsData(data)
let item = self.getItem(name)
completion(item)
} catch {
os_log("OpenHABItemCache %{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
}
case let .failure(error):
print(error)
os_log("OpenHABItemCache %{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
}
}
}
case let .failure(error):
if self.lastUrlConnected == OpenHABItemCache.URL_LOCAL {
self.localUrlFailed = true
os_log("%{PUBLIC}@ ", log: .default, type: .info, error.localizedDescription)
self.reload(name: name, completion: completion) // try remote

} else {
os_log("%{PUBLIC}@ ", log: .default, type: .error, error.localizedDescription)
}
}
}
}

func getURL() -> URL? {
var uurl: URL?

if Preferences.demomode {
uurl = Endpoint.items(openHABRootUrl: "https://demo.openhab.org").url
url = uurl?.absoluteString ?? "unknown"
lastUrlConnected = OpenHABItemCache.URL_DEMO

} else {
if localUrlFailed {
uurl = Endpoint.items(openHABRootUrl: Preferences.remoteUrl).url
url = uurl?.absoluteString ?? "unknown"
lastUrlConnected = OpenHABItemCache.URL_REMOTE

} else {
uurl = Endpoint.items(openHABRootUrl: Preferences.localUrl).url
url = uurl?.absoluteString ?? "unknown"
lastUrlConnected = OpenHABItemCache.URL_LOCAL
}
}

return uurl
.store(in: &cancellables)
}

private func decodeItemsData(_ data: Data) throws {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
let codingDatas = try data.decoded(as: [OpenHABItem.CodingData].self, using: decoder)

items = [OpenHABItem]()

for codingDatum in codingDatas where codingDatum.openHABItem.type != OpenHABItem.ItemType.group {
self.items?.append(codingDatum.openHABItem)
}

os_log("Loaded items to cache: %{PUBLIC}d", log: .default, type: .info, items?.count ?? 0)
}
}
Loading

0 comments on commit e0069b4

Please sign in to comment.