Skip to content
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
8 changes: 8 additions & 0 deletions ORLib.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
914F7D4429215D3500655A22 /* test12.json in Resources */ = {isa = PBXBuildFile; fileRef = 914F7D4329215D3500655A22 /* test12.json */; };
9154E2A02D9EB0D50055E565 /* StringUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */; };
9154E2A22D9EB3220055E565 /* URLTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9154E2A12D9EB3220055E565 /* URLTest.swift */; };
91E8A0032F07700000E5BF01 /* ESPProvisionURLResolverTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91E8A0022F07700000E5BF01 /* ESPProvisionURLResolverTest.swift */; };
9156512A28FC6D6700062E16 /* test9.json in Resources */ = {isa = PBXBuildFile; fileRef = 9156512928FC6D6700062E16 /* test9.json */; };
915CCC952EF163E8006B65AA /* DeviceProvisionAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915CCC942EF163E8006B65AA /* DeviceProvisionAPI.swift */; };
915CCC972EF16456006B65AA /* DeviceProvision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915CCC962EF16456006B65AA /* DeviceProvision.swift */; };
Expand All @@ -71,6 +72,7 @@
91F2165C2D887BCF008F9CA7 /* ORConfigChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F216552D887BCF008F9CA7 /* ORConfigChannel.swift */; };
91F2165E2D887BCF008F9CA7 /* DeviceRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F216522D887BCF008F9CA7 /* DeviceRegistry.swift */; };
91F2165F2D887BCF008F9CA7 /* ORESPDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F216572D887BCF008F9CA7 /* ORESPDevice.swift */; };
91E8A0012F07700000E5BF01 /* ESPProvisionURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91E8A0002F07700000E5BF01 /* ESPProvisionURLResolver.swift */; };
91F216602D887BCF008F9CA7 /* WifiProvisioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F216582D887BCF008F9CA7 /* WifiProvisioner.swift */; };
91F216612D887BCF008F9CA7 /* ESPProvisionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F216532D887BCF008F9CA7 /* ESPProvisionProvider.swift */; };
91F216622D887BCF008F9CA7 /* ORConfigChannelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F216562D887BCF008F9CA7 /* ORConfigChannelProtocol.swift */; };
Expand Down Expand Up @@ -145,6 +147,7 @@
914F7D4329215D3500655A22 /* test12.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = test12.json; sourceTree = "<group>"; };
9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtilsTest.swift; sourceTree = "<group>"; };
9154E2A12D9EB3220055E565 /* URLTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTest.swift; sourceTree = "<group>"; };
91E8A0022F07700000E5BF01 /* ESPProvisionURLResolverTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESPProvisionURLResolverTest.swift; sourceTree = "<group>"; };
9156512928FC6D6700062E16 /* test9.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = test9.json; sourceTree = "<group>"; };
915CCC942EF163E8006B65AA /* DeviceProvisionAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProvisionAPI.swift; sourceTree = "<group>"; };
915CCC962EF16456006B65AA /* DeviceProvision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProvision.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -181,6 +184,7 @@
91F216562D887BCF008F9CA7 /* ORConfigChannelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORConfigChannelProtocol.swift; sourceTree = "<group>"; };
91F216572D887BCF008F9CA7 /* ORESPDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ORESPDevice.swift; sourceTree = "<group>"; };
91F216582D887BCF008F9CA7 /* WifiProvisioner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiProvisioner.swift; sourceTree = "<group>"; };
91E8A0002F07700000E5BF01 /* ESPProvisionURLResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESPProvisionURLResolver.swift; sourceTree = "<group>"; };
91F216712D887BCF008F9CA7 /* TimeSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSource.swift; sourceTree = "<group>"; };
91F216652D887C53008F9CA7 /* DeviceProvisionAPIMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProvisionAPIMock.swift; sourceTree = "<group>"; };
91F216662D887C53008F9CA7 /* ESPProvisionProviderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESPProvisionProviderTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -359,6 +363,7 @@
91F216732D887C53008F9CA7 /* TestTimeSource.swift */,
9154E29F2D9EB0D50055E565 /* StringUtilsTest.swift */,
9154E2A12D9EB3220055E565 /* URLTest.swift */,
91E8A0022F07700000E5BF01 /* ESPProvisionURLResolverTest.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand All @@ -375,6 +380,7 @@
91F216562D887BCF008F9CA7 /* ORConfigChannelProtocol.swift */,
91F216572D887BCF008F9CA7 /* ORESPDevice.swift */,
91F216582D887BCF008F9CA7 /* WifiProvisioner.swift */,
91E8A0002F07700000E5BF01 /* ESPProvisionURLResolver.swift */,
91F216712D887BCF008F9CA7 /* TimeSource.swift */,
91DD55A02DE7306100957233 /* BLEPermissionsChecker.swift */,
915CCC942EF163E8006B65AA /* DeviceProvisionAPI.swift */,
Expand Down Expand Up @@ -562,6 +568,7 @@
91F216622D887BCF008F9CA7 /* ORConfigChannelProtocol.swift in Sources */,
91F216632D887BCF008F9CA7 /* DeviceConnection.swift in Sources */,
91F216702D887BCF008F9CA7 /* TimeSource.swift in Sources */,
91E8A0012F07700000E5BF01 /* ESPProvisionURLResolver.swift in Sources */,
915CCC952EF163E8006B65AA /* DeviceProvisionAPI.swift in Sources */,
91F216642D887BCF008F9CA7 /* CallbackChannel.swift in Sources */,
4CBDF2CA2AE2869D00C7D94C /* ApiManager.swift in Sources */,
Expand Down Expand Up @@ -594,6 +601,7 @@
91F216722D887C53008F9CA7 /* TestTimeSource.swift in Sources */,
91A9A90128BF6EA000DF8928 /* FileApiManager.swift in Sources */,
9154E2A22D9EB3220055E565 /* URLTest.swift in Sources */,
91E8A0032F07700000E5BF01 /* ESPProvisionURLResolverTest.swift in Sources */,
9154E2A02D9EB0D50055E565 /* StringUtilsTest.swift in Sources */,
91A9A8FB28BF6A4900DF8928 /* ConfigManagerTest.swift in Sources */,
91AA79F328D628E9005B9913 /* Fixture.swift in Sources */,
Expand Down
9 changes: 7 additions & 2 deletions ORLib/ConsoleProviders/ESPProvision/DeviceConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,17 @@ class DeviceConnection {
}
}

func sendOpenRemoteConfig(mqttBrokerUrl: String, mqttUser: String, mqttPassword: String, assetId: String, properties: [String: String] = [:]) async throws {
func sendOpenRemoteConfig(mqttBrokerUrl: String, mqttUser: String, mqttPassword: String, realm: String, assetId: String, properties: [String: String] = [:]) async throws {
if !isConnected {
throw ESPProviderError(errorCode: .notConnected, errorMessage: "No connection established to device")
}
do {
try await configChannel!.sendOpenRemoteConfig(mqttBrokerUrl: mqttBrokerUrl, mqttUser: mqttUser, mqttPassword: mqttPassword, assetId: assetId, properties: properties)
try await configChannel!.sendOpenRemoteConfig(mqttBrokerUrl: mqttBrokerUrl,
mqttUser: mqttUser,
mqttPassword: mqttPassword,
realm: realm,
assetId: assetId,
properties: properties)
} catch {
throw ESPProviderError(errorCode: .communicationError, errorMessage: error.localizedDescription)
}
Expand Down
21 changes: 12 additions & 9 deletions ORLib/ConsoleProviders/ESPProvision/DeviceProvision.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import os
import RandomPasswordGenerator

protocol DeviceProvisionAPI {
func provision(modelName: String, deviceId: String, password: String, token: String) async throws -> ProvisionResult
func provision(apiURL: URL, modelName: String, deviceId: String, password: String, token: String) async throws -> ProvisionResult
}

struct ProvisionResult {
Expand All @@ -40,20 +40,18 @@ class DeviceProvision {
var callbackChannel: CallbackChannel?

private let timeSource: any TimeSource
var apiURL: URL
var deviceProvisionAPI: DeviceProvisionAPI

var backendConnectionTimeout: TimeInterval = 60

init (deviceConnection: DeviceConnection?, callbackChannel: CallbackChannel?, apiURL: URL, timeSource: any TimeSource = SystemTimeSource()) {
init (deviceConnection: DeviceConnection?, callbackChannel: CallbackChannel?, timeSource: any TimeSource = SystemTimeSource()) {
self.deviceConnection = deviceConnection
self.callbackChannel = callbackChannel
self.timeSource = timeSource
self.apiURL = apiURL
self.deviceProvisionAPI = DeviceProvisionAPIREST(apiURL: apiURL)
self.deviceProvisionAPI = DeviceProvisionAPIREST()
}

public func provision(userToken: String) async {
public func provision(apiURL: URL, realm: String, userToken: String) async {
guard deviceConnection?.isConnected ?? false else {
sendProvisionDeviceStatus(connected: false, error: .notConnected, errorMessage: "No connection established to device")
return
Expand All @@ -65,12 +63,17 @@ class DeviceProvision {

let password = try generatePassword()

let result = try await deviceProvisionAPI.provision(modelName: deviceInfo.modelName, deviceId: deviceInfo.deviceId, password: password, token: userToken)
let result = try await deviceProvisionAPI.provision(apiURL: apiURL,
modelName: deviceInfo.modelName,
deviceId: deviceInfo.deviceId,
password: password,
token: userToken)
let userName = deviceInfo.deviceId.lowercased(with: Locale(identifier: "en"))

try await deviceConnection!.sendOpenRemoteConfig(mqttBrokerUrl: mqttURL,
try await deviceConnection!.sendOpenRemoteConfig(mqttBrokerUrl: mqttURL(apiURL: apiURL),
mqttUser: userName,
mqttPassword: password,
realm: realm,
assetId: result.assetId,
properties: result.properties)

Expand Down Expand Up @@ -132,7 +135,7 @@ class DeviceProvision {
return try generator.generate()
}

private var mqttURL: String {
private func mqttURL(apiURL: URL) -> String {
// TODO: is this OK or do we want to get the mqtt url from the server

return "mqtts://\(apiURL.host ?? "localhost"):8883"
Expand Down
8 changes: 1 addition & 7 deletions ORLib/ConsoleProviders/ESPProvision/DeviceProvisionAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,7 @@ import os

struct DeviceProvisionAPIREST: DeviceProvisionAPI {

init(apiURL: URL) {
self.apiURL = apiURL
}

private var apiURL: URL

func provision(modelName: String, deviceId: String, password: String, token: String) async throws -> ProvisionResult {
func provision(apiURL: URL, modelName: String, deviceId: String, password: String, token: String) async throws -> ProvisionResult {
/*
curl -v http://localhost:8080/api/master/rest/battery -d'{
"model": 0,
Expand Down
16 changes: 4 additions & 12 deletions ORLib/ConsoleProviders/ESPProvision/ESPProvisionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,11 @@ class ESPProvisionProvider: NSObject {

private var wifiProvisioner: WifiProvisioner?

private var apiURL = URL(string: "http://localhost:8080/api/master")!

private var blePermissionsChecker: BLEPermissionsChecker?

typealias DeviceProvisionFactory = () -> DeviceProvision
private lazy var deviceProvisionFactory: DeviceProvisionFactory = {
DeviceProvision(deviceConnection: self.deviceConnection, callbackChannel: self.callbackChannel, apiURL: self.apiURL, timeSource: self.timeSource)
DeviceProvision(deviceConnection: self.deviceConnection, callbackChannel: self.callbackChannel, timeSource: self.timeSource)
}

private var callbackChannel: CallbackChannel?
Expand All @@ -76,11 +74,6 @@ class ESPProvisionProvider: NSObject {
self.init(timeSource: SystemTimeSource())
}

public convenience init(apiURL: URL = URL(string: "http://localhost:8080/api/master")!) {
self.init(timeSource: SystemTimeSource())
self.apiURL = apiURL
}

// MARK: Standard provider lifecycle

public func initialize() -> [String: Any] {
Expand Down Expand Up @@ -205,7 +198,7 @@ class ESPProvisionProvider: NSObject {

// MARK: OR Configuration

public func provisionDevice(userToken: String) {
public func provisionDevice(apiURL: URL, realm: String, userToken: String) {
guard deviceConnection?.isConnected ?? false else {
sendProvisionDeviceError(.notConnected, errorMessage: "No connection established to device")
return
Expand All @@ -214,7 +207,7 @@ class ESPProvisionProvider: NSObject {
do {
let deviceProvision = deviceProvisionFactory()

try await deviceProvision.provision(userToken: userToken)
try await deviceProvision.provision(apiURL: apiURL, realm: realm, userToken: userToken)
} catch let error as ESPProviderError {
sendExitProvisioningError(error.errorCode, errorMessage: error.errorMessage)
} catch {
Expand All @@ -238,7 +231,6 @@ extension ESPProvisionProvider {
public convenience init(searchDeviceTimeout: TimeInterval = 120, searchDeviceMaxIterations: Int = 25,
searchWifiTimeout: TimeInterval = 120, searchWifiMaxIterations: Int = 25,
deviceProvisionAPI: DeviceProvisionAPI? = nil, backendConnectionTimeout: TimeInterval? = nil,
apiURL: URL = URL(string: "http://localhost:8080/api/master")!,
timeSource: (any TimeSource)? = nil) {
self.init(timeSource: timeSource ?? SystemTimeSource())
self.searchDeviceTimeout = searchDeviceTimeout
Expand All @@ -250,7 +242,7 @@ extension ESPProvisionProvider {
self.searchWifiMaxIterations = searchWifiMaxIterations

self.deviceProvisionFactory = {
let deviceProvision = DeviceProvision(deviceConnection: self.deviceConnection, callbackChannel: self.callbackChannel, apiURL: apiURL, timeSource: self.timeSource)
let deviceProvision = DeviceProvision(deviceConnection: self.deviceConnection, callbackChannel: self.callbackChannel, timeSource: self.timeSource)
if let deviceProvisionAPI {
deviceProvision.deviceProvisionAPI = deviceProvisionAPI
}
Expand Down
103 changes: 103 additions & 0 deletions ORLib/ConsoleProviders/ESPProvision/ESPProvisionURLResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2025, OpenRemote Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import Foundation

struct ESPProvisionURLResolver {
private static let realmQueryKey = "realm"
private static let realmPathAllowedCharacters: CharacterSet = {
var allowedCharacters = CharacterSet.urlPathAllowed
allowedCharacters.remove(charactersIn: "/%")
return allowedCharacters
}()

private let userDefaults: UserDefaults?

init(userDefaults: UserDefaults? = UserDefaults(suiteName: DefaultsKey.groupEntitlement)) {
self.userDefaults = userDefaults
}

func realm(currentURL: URL?, targetUrl: String?, baseUrl: String?) -> String {
if let realm = realmFromURLs(currentURL: currentURL, targetUrl: targetUrl, baseUrl: baseUrl) {
return realm
}

if let realm = userDefaults?.string(forKey: DefaultsKey.realmKey),
!realm.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return realm
}

return "master"
}

func apiURL(baseUrl: String, realm: String) -> URL? {
guard let appUrl = makeURL(from: baseUrl),
var components = URLComponents(url: appUrl, resolvingAgainstBaseURL: false),
let encodedRealm = realm.addingPercentEncoding(withAllowedCharacters: Self.realmPathAllowedCharacters) else {
return nil
}

components.percentEncodedPath = "/api/\(encodedRealm)"
components.query = nil
components.fragment = nil
return components.url
Comment thread
MartinaeyNL marked this conversation as resolved.
}

func apiURL(currentURL: URL?, targetUrl: String?, baseUrl: String?, realm: String) -> URL? {
let urls = [
baseUrl.flatMap { makeURL(from: $0) },
currentURL,
targetUrl.flatMap { makeURL(from: $0) }
]

for url in urls {
guard let url,
let apiURL = apiURL(baseUrl: url.absoluteString, realm: realm) else {
continue
}
return apiURL
}

return nil
}

private func realmFromURLs(currentURL: URL?, targetUrl: String?, baseUrl: String?) -> String? {
let urls = [
currentURL,
targetUrl.flatMap { makeURL(from: $0) },
baseUrl.flatMap { makeURL(from: $0) }
]

for url in urls {
guard let url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let realm = components.queryItems?.first(where: { $0.name == Self.realmQueryKey })?.value,
!realm.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
continue
}
return realm
}

return nil
}

private func makeURL(from urlString: String) -> URL? {
URL(string: urlString) ?? urlString.stringByURLEncoding().flatMap { URL(string: $0) }
}
}
Loading
Loading