feat: Begins using swift argument parser and creating cli client dependency
All checks were successful
CI / Run Tests (push) Successful in 4m27s
All checks were successful
CI / Run Tests (push) Successful in 4m27s
This commit is contained in:
2
.swiftlint.yml
Normal file
2
.swiftlint.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
disabled_rules:
|
||||||
|
- closing_brace
|
||||||
@@ -17,17 +17,11 @@
|
|||||||
},
|
},
|
||||||
"testTargets" : [
|
"testTargets" : [
|
||||||
{
|
{
|
||||||
|
"parallelizable" : true,
|
||||||
"target" : {
|
"target" : {
|
||||||
"containerPath" : "container:",
|
"containerPath" : "container:",
|
||||||
"identifier" : "MQTTConnectionServiceTests",
|
"identifier" : "IntegrationTests",
|
||||||
"name" : "MQTTConnectionServiceTests"
|
"name" : "IntegrationTests"
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target" : {
|
|
||||||
"containerPath" : "container:",
|
|
||||||
"identifier" : "SensorsServiceTests",
|
|
||||||
"name" : "SensorsServiceTests"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -77,6 +77,34 @@
|
|||||||
ReferencedContainer = "container:">
|
ReferencedContainer = "container:">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "IntegrationTests"
|
||||||
|
BuildableName = "IntegrationTests"
|
||||||
|
BlueprintName = "IntegrationTests"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CliClient"
|
||||||
|
BuildableName = "CliClient"
|
||||||
|
BlueprintName = "CliClient"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
@@ -95,19 +123,9 @@
|
|||||||
skipped = "NO">
|
skipped = "NO">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "MQTTConnectionServiceTests"
|
BlueprintIdentifier = "IntegrationTests"
|
||||||
BuildableName = "MQTTConnectionServiceTests"
|
BuildableName = "IntegrationTests"
|
||||||
BlueprintName = "MQTTConnectionServiceTests"
|
BlueprintName = "IntegrationTests"
|
||||||
ReferencedContainer = "container:">
|
|
||||||
</BuildableReference>
|
|
||||||
</TestableReference>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "SensorsServiceTests"
|
|
||||||
BuildableName = "SensorsServiceTests"
|
|
||||||
BlueprintName = "SensorsServiceTests"
|
|
||||||
ReferencedContainer = "container:">
|
ReferencedContainer = "container:">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
|
|||||||
22
Makefile
22
Makefile
@@ -1,29 +1,39 @@
|
|||||||
|
DOCKER_IMAGE_NAME?="swift-mqtt-dewpoint"
|
||||||
|
DOCKER_TAG_NAME?="latest"
|
||||||
|
|
||||||
bootstrap-env:
|
.PHONY: bootstrap
|
||||||
|
bootstrap:
|
||||||
@cp Bootstrap/dewPoint-env-example .dewPoint-env
|
@cp Bootstrap/dewPoint-env-example .dewPoint-env
|
||||||
|
|
||||||
bootstrap-topics:
|
.PHONY: build
|
||||||
@cp Bootstrap/topics-example .topics
|
|
||||||
|
|
||||||
bootstrap: bootstrap-env bootstrap-topics
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@swift build -Xswiftc -strict-concurrency=complete
|
@swift build -Xswiftc -strict-concurrency=complete
|
||||||
|
|
||||||
|
.PHONY: build-docker
|
||||||
|
build-docker:
|
||||||
|
@docker build \
|
||||||
|
--file docker/Dockerfile \
|
||||||
|
--tag "${DOCKER_IMAGE_NAME}:${DOCKER_TAG_NAME}" .
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf .build
|
rm -rf .build
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
run:
|
run:
|
||||||
@swift run dewpoint-controller
|
@swift run dewpoint-controller
|
||||||
|
|
||||||
|
.PHONY: test-docker
|
||||||
test-docker:
|
test-docker:
|
||||||
@docker compose --file docker/docker-compose-test.yaml \
|
@docker compose --file docker/docker-compose-test.yaml \
|
||||||
run --build --remove-orphans -i --rm test
|
run --build --remove-orphans -i --rm test
|
||||||
@docker compose --file docker/docker-compose-test.yaml down
|
@docker compose --file docker/docker-compose-test.yaml down
|
||||||
|
|
||||||
|
.PHONY: start-mosquitto
|
||||||
start-mosquitto:
|
start-mosquitto:
|
||||||
@docker compose --file docker/docker-compose.yaml \
|
@docker compose --file docker/docker-compose.yaml \
|
||||||
up -d mosquitto
|
up -d mosquitto
|
||||||
|
|
||||||
|
.PHONY: test-swift
|
||||||
test-swift: start-mosquitto
|
test-swift: start-mosquitto
|
||||||
@swift test --enable-code-coverage
|
@swift test --enable-code-coverage
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "33fdcea7245de36c7e638047a16bba6605bc9bac0117aab7cb9397289a33214e",
|
"originHash" : "486be5d69e4f0ba7b9f42046df31a727c7e394e4ecfae5671e1b194bed7c9e9b",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "combine-schedulers",
|
"identity" : "combine-schedulers",
|
||||||
@@ -10,6 +10,15 @@
|
|||||||
"version" : "1.0.2"
|
"version" : "1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dotenv",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/swiftpackages/DotEnv.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "1f15bb9de727d694af1d003a1a5d7a553752850f",
|
||||||
|
"version" : "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "mqtt-nio",
|
"identity" : "mqtt-nio",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -19,6 +28,15 @@
|
|||||||
"version" : "2.11.0"
|
"version" : "2.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-argument-parser",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
|
||||||
|
"version" : "1.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-async-algorithms",
|
"identity" : "swift-async-algorithms",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -64,6 +82,15 @@
|
|||||||
"version" : "1.3.0"
|
"version" : "1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-custom-dump",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||||
|
"version" : "1.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-dependencies",
|
"identity" : "swift-dependencies",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -14,30 +14,43 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.executable(name: "dewpoint-controller", targets: ["DewPointController"]),
|
.executable(name: "dewpoint-controller", targets: ["DewPointController"]),
|
||||||
|
.library(name: "CliClient", targets: ["CliClient"]),
|
||||||
.library(name: "Models", targets: ["Models"]),
|
.library(name: "Models", targets: ["Models"]),
|
||||||
.library(name: "MQTTManager", targets: ["MQTTManager"]),
|
.library(name: "MQTTManager", targets: ["MQTTManager"]),
|
||||||
.library(name: "MQTTConnectionService", targets: ["MQTTConnectionService"]),
|
.library(name: "MQTTConnectionService", targets: ["MQTTConnectionService"]),
|
||||||
.library(name: "SensorsService", targets: ["SensorsService"])
|
.library(name: "SensorsService", targets: ["SensorsService"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
||||||
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
|
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
|
||||||
.package(url: "https://github.com/apple/swift-nio", from: "2.0.0"),
|
|
||||||
.package(url: "https://github.com/apple/swift-log", from: "1.6.0"),
|
.package(url: "https://github.com/apple/swift-log", from: "1.6.0"),
|
||||||
|
.package(url: "https://github.com/swiftpackages/DotEnv.git", from: "3.0.0"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
|
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
|
||||||
|
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
|
||||||
.package(url: "https://github.com/swift-psychrometrics/swift-psychrometrics", exact: "0.2.3"),
|
.package(url: "https://github.com/swift-psychrometrics/swift-psychrometrics", exact: "0.2.3"),
|
||||||
.package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.0.0"),
|
.package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.0.0"),
|
||||||
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0")
|
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "CliClient",
|
||||||
|
dependencies: [
|
||||||
|
"Models",
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
|
.product(name: "DotEnv", package: "DotEnv"),
|
||||||
|
.product(name: "MQTTNIO", package: "mqtt-nio")
|
||||||
|
]
|
||||||
|
),
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "DewPointController",
|
name: "DewPointController",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"Models",
|
"CliClient",
|
||||||
"MQTTManager",
|
|
||||||
"MQTTConnectionService",
|
"MQTTConnectionService",
|
||||||
"SensorsService",
|
"SensorsService",
|
||||||
.product(name: "MQTTNIO", package: "mqtt-nio"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
.product(name: "NIO", package: "swift-nio"),
|
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||||
|
// .product(name: "DotEnv", package: "DotEnv"),
|
||||||
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics")
|
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
@@ -69,10 +82,12 @@ let package = Package(
|
|||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "MQTTConnectionServiceTests",
|
name: "IntegrationTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
"DewPointController",
|
||||||
"MQTTConnectionService",
|
"MQTTConnectionService",
|
||||||
"MQTTManager",
|
"MQTTManager",
|
||||||
|
"SensorsService",
|
||||||
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics"),
|
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics"),
|
||||||
.product(name: "ServiceLifecycleTestKit", package: "swift-service-lifecycle")
|
.product(name: "ServiceLifecycleTestKit", package: "swift-service-lifecycle")
|
||||||
]
|
]
|
||||||
@@ -89,13 +104,6 @@ let package = Package(
|
|||||||
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle")
|
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle")
|
||||||
],
|
],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "SensorsServiceTests",
|
|
||||||
dependencies: [
|
|
||||||
"SensorsService",
|
|
||||||
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics")
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
188
Sources/CliClient/CliClient.swift
Normal file
188
Sources/CliClient/CliClient.swift
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import DotEnv
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import Models
|
||||||
|
import MQTTNIO
|
||||||
|
import NIO
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
var cliClient: CliClient {
|
||||||
|
get { self[CliClient.self] }
|
||||||
|
set { self[CliClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DependencyClient
|
||||||
|
public struct CliClient {
|
||||||
|
|
||||||
|
public var logLevel: @Sendable (EnvVars) -> Logger.Level = { _ in .debug }
|
||||||
|
public var makeEnvVars: @Sendable (EnvVarsRequest) async throws -> EnvVars
|
||||||
|
public var makeClient: @Sendable (ClientRequest) throws -> MQTTClient
|
||||||
|
public var parseMqttClientVersion: @Sendable (String) -> MQTTClient.Version?
|
||||||
|
|
||||||
|
public struct ClientRequest: Sendable {
|
||||||
|
public let envVars: EnvVars
|
||||||
|
public let eventLoopGroup: MultiThreadedEventLoopGroup
|
||||||
|
public let logger: Logger?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
envVars: EnvVars,
|
||||||
|
eventLoopGroup: MultiThreadedEventLoopGroup,
|
||||||
|
logger: Logger?
|
||||||
|
) {
|
||||||
|
self.envVars = envVars
|
||||||
|
self.eventLoopGroup = eventLoopGroup
|
||||||
|
self.logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EnvVarsRequest: Sendable {
|
||||||
|
|
||||||
|
public let envFilePath: String?
|
||||||
|
public let logger: Logger?
|
||||||
|
public let mqttClientVersion: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
envFilePath: String? = nil,
|
||||||
|
logger: Logger? = nil,
|
||||||
|
version mqttClientVersion: String? = nil
|
||||||
|
) {
|
||||||
|
self.envFilePath = envFilePath
|
||||||
|
self.logger = logger
|
||||||
|
self.mqttClientVersion = mqttClientVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliClient: DependencyKey {
|
||||||
|
public static let testValue: CliClient = Self()
|
||||||
|
public static var liveValue: CliClient {
|
||||||
|
Self(
|
||||||
|
logLevel: { Logger.Level.from(environment: $0) },
|
||||||
|
makeEnvVars: {
|
||||||
|
try EnvVars.load(
|
||||||
|
dotEnvFile: $0.envFilePath,
|
||||||
|
logger: $0.logger,
|
||||||
|
version: $0.mqttClientVersion
|
||||||
|
)
|
||||||
|
},
|
||||||
|
makeClient: {
|
||||||
|
MQTTClient(
|
||||||
|
envVars: $0.envVars,
|
||||||
|
eventLoopGroup: $0.eventLoopGroup,
|
||||||
|
logger: $0.logger
|
||||||
|
)
|
||||||
|
},
|
||||||
|
parseMqttClientVersion: { .init(string: $0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvVars {
|
||||||
|
|
||||||
|
/// Load the `EnvVars` from the environment.
|
||||||
|
///
|
||||||
|
/// - Paramaters:
|
||||||
|
/// - logger: An optional logger to use for debugging.
|
||||||
|
/// - version: A version that is specified from command line, ignoring any environment variable.
|
||||||
|
static func load(
|
||||||
|
dotEnvFile: String?,
|
||||||
|
logger: Logger?,
|
||||||
|
version: String?
|
||||||
|
) throws -> EnvVars {
|
||||||
|
let defaultEnvVars = EnvVars()
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
if let dotEnvFile {
|
||||||
|
try DotEnv.load(path: dotEnvFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultEnvDict = (try? encoder.encode(defaultEnvVars))
|
||||||
|
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||||
|
?? [:]
|
||||||
|
|
||||||
|
let envVarsDict = defaultEnvDict
|
||||||
|
.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
|
||||||
|
|
||||||
|
var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
|
||||||
|
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
||||||
|
?? defaultEnvVars
|
||||||
|
|
||||||
|
if let version {
|
||||||
|
envVars.version = version
|
||||||
|
}
|
||||||
|
|
||||||
|
logger?.debug("Done loading EnvVars...")
|
||||||
|
|
||||||
|
return envVars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension MQTTClient {
|
||||||
|
convenience init(
|
||||||
|
envVars: EnvVars,
|
||||||
|
eventLoopGroup: EventLoopGroup,
|
||||||
|
logger: Logger?
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
host: envVars.host,
|
||||||
|
port: envVars.port != nil ? Int(envVars.port!) : nil,
|
||||||
|
identifier: envVars.identifier,
|
||||||
|
eventLoopGroupProvider: .shared(eventLoopGroup),
|
||||||
|
logger: logger,
|
||||||
|
configuration: .init(
|
||||||
|
version: .parseOrDefualt(string: envVars.version),
|
||||||
|
disablePing: false,
|
||||||
|
userName: envVars.userName,
|
||||||
|
password: envVars.password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MQTTClient.Version {
|
||||||
|
static let `default` = Self.v3_1_1
|
||||||
|
|
||||||
|
static func parseOrDefualt(string: String?) -> Self {
|
||||||
|
guard let string, let value = Self(string: string) else {
|
||||||
|
return .default
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(string: String) {
|
||||||
|
if string.contains("5") {
|
||||||
|
self = .v5_0
|
||||||
|
} else if string.contains("3") {
|
||||||
|
self = .v3_1_1
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Logger.Level {
|
||||||
|
|
||||||
|
/// Parse a `Logger.Level` from the loaded `EnvVars`.
|
||||||
|
static func from(environment envVars: EnvVars) -> Self {
|
||||||
|
// If the log level was set via an environment variable.
|
||||||
|
if let logLevel = envVars.logLevel {
|
||||||
|
return logLevel
|
||||||
|
}
|
||||||
|
// Parse the appEnv to derive an log level.
|
||||||
|
switch envVars.appEnv {
|
||||||
|
case .staging, .development:
|
||||||
|
return .debug
|
||||||
|
case .production:
|
||||||
|
return .info
|
||||||
|
case .testing:
|
||||||
|
return .trace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ArgumentParser
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
@@ -11,106 +12,10 @@ import SensorsService
|
|||||||
import ServiceLifecycle
|
import ServiceLifecycle
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Application {
|
struct Application: AsyncParsableCommand {
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
/// The main entry point of the application.
|
commandName: "dewpoint-controller",
|
||||||
static func main() async throws {
|
abstract: "Command for running the dewpoint mqtt service.",
|
||||||
let eventloopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
subcommands: [Run.self, Debug.self]
|
||||||
var logger = Logger(label: "dewpoint-controller")
|
)
|
||||||
logger.logLevel = .trace
|
|
||||||
|
|
||||||
logger.info("Starting dewpoint-controller!")
|
|
||||||
|
|
||||||
let environment = loadEnvVars(logger: logger)
|
|
||||||
|
|
||||||
if environment.appEnv == .production {
|
|
||||||
logger.debug("Updating logging level to info.")
|
|
||||||
logger.logLevel = .info
|
|
||||||
}
|
|
||||||
|
|
||||||
let mqtt = MQTTClient(
|
|
||||||
envVars: environment,
|
|
||||||
eventLoopGroup: eventloopGroup,
|
|
||||||
logger: logger
|
|
||||||
)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try await withDependencies {
|
|
||||||
$0.psychrometricClient = .liveValue
|
|
||||||
$0.mqtt = .live(client: mqtt, logger: logger)
|
|
||||||
} operation: {
|
|
||||||
let mqttConnection = MQTTConnectionService(logger: logger)
|
|
||||||
let sensors = SensorsService(sensors: .live, logger: logger)
|
|
||||||
|
|
||||||
var serviceGroupConfiguration = ServiceGroupConfiguration(
|
|
||||||
services: [
|
|
||||||
mqttConnection,
|
|
||||||
sensors
|
|
||||||
],
|
|
||||||
gracefulShutdownSignals: [.sigterm, .sigint],
|
|
||||||
logger: logger
|
|
||||||
)
|
|
||||||
serviceGroupConfiguration.maximumCancellationDuration = .seconds(5)
|
|
||||||
serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10)
|
|
||||||
|
|
||||||
let serviceGroup = ServiceGroup(configuration: serviceGroupConfiguration)
|
|
||||||
|
|
||||||
try await serviceGroup.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
try await mqtt.shutdown()
|
|
||||||
try await eventloopGroup.shutdownGracefully()
|
|
||||||
} catch {
|
|
||||||
try await eventloopGroup.shutdownGracefully()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
|
|
||||||
private func loadEnvVars(logger: Logger) -> EnvVars {
|
|
||||||
let defaultEnvVars = EnvVars()
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
|
|
||||||
let defaultEnvDict = (try? encoder.encode(defaultEnvVars))
|
|
||||||
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
|
||||||
?? [:]
|
|
||||||
|
|
||||||
let envVarsDict = defaultEnvDict
|
|
||||||
.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
|
|
||||||
|
|
||||||
let envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
|
|
||||||
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
|
||||||
?? defaultEnvVars
|
|
||||||
|
|
||||||
logger.debug("Done loading EnvVars...")
|
|
||||||
|
|
||||||
return envVars
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension MQTTNIO.MQTTClient {
|
|
||||||
convenience init(envVars: EnvVars, eventLoopGroup: EventLoopGroup, logger: Logger?) {
|
|
||||||
self.init(
|
|
||||||
host: envVars.host,
|
|
||||||
port: envVars.port != nil ? Int(envVars.port!) : nil,
|
|
||||||
identifier: envVars.identifier,
|
|
||||||
eventLoopGroupProvider: .shared(eventLoopGroup),
|
|
||||||
logger: logger,
|
|
||||||
configuration: .init(
|
|
||||||
version: .v3_1_1,
|
|
||||||
disablePing: false,
|
|
||||||
userName: envVars.userName,
|
|
||||||
password: envVars.password
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Array where Element == TemperatureAndHumiditySensor {
|
|
||||||
static var live: Self {
|
|
||||||
TemperatureAndHumiditySensor.Location.allCases.map { location in
|
|
||||||
TemperatureAndHumiditySensor(location: location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
74
Sources/DewPointController/DebugCommand.swift
Normal file
74
Sources/DewPointController/DebugCommand.swift
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import CustomDump
|
||||||
|
import Dependencies
|
||||||
|
import DotEnv
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import Models
|
||||||
|
|
||||||
|
extension Application {
|
||||||
|
|
||||||
|
struct Debug: AsyncParsableCommand {
|
||||||
|
|
||||||
|
static let configuration: CommandConfiguration = .init(
|
||||||
|
commandName: "debug",
|
||||||
|
abstract: "Debug the environment variables."
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup
|
||||||
|
var shared: SharedOptions
|
||||||
|
|
||||||
|
@Flag(
|
||||||
|
name: [.customLong("show-password")],
|
||||||
|
help: "Don't redact the password from the console."
|
||||||
|
)
|
||||||
|
var showPassword: Bool = false
|
||||||
|
|
||||||
|
mutating func run() async throws {
|
||||||
|
@Dependency(\.cliClient) var client
|
||||||
|
let logger = Logger(label: "debug-command")
|
||||||
|
|
||||||
|
print("--------------------------")
|
||||||
|
print("Running debug command...")
|
||||||
|
if let envFile = shared.envFile {
|
||||||
|
print("Reading env file: \(envFile)")
|
||||||
|
print("--------------------------")
|
||||||
|
} else {
|
||||||
|
print("No env file set.")
|
||||||
|
print("--------------------------")
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Loading EnvVars")
|
||||||
|
print("--------------------------")
|
||||||
|
let envVars = try await client.makeEnvVars(shared.envVarsRequest(logger: logger))
|
||||||
|
printEnvVars(envVars: envVars, showPassword: showPassword)
|
||||||
|
print("--------------------------")
|
||||||
|
|
||||||
|
if let logLevel = shared.logLevel, let level = logLevel() {
|
||||||
|
print("Log Level option: \(level)")
|
||||||
|
print("--------------------------")
|
||||||
|
} else {
|
||||||
|
print("Log Level option: nil")
|
||||||
|
print("--------------------------")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func printEnvVars(envVars: EnvVars, showPassword: Bool) {
|
||||||
|
// show the proper password to show depending on if it exists
|
||||||
|
// and if we should redact it or not.
|
||||||
|
var passwordString: String?
|
||||||
|
switch (showPassword, envVars.password) {
|
||||||
|
case (true, .none), (_, .none):
|
||||||
|
break
|
||||||
|
case (true, let .some(password)):
|
||||||
|
passwordString = password
|
||||||
|
case (false, .some):
|
||||||
|
passwordString = "<redacted>"
|
||||||
|
}
|
||||||
|
var envVars = envVars
|
||||||
|
envVars.password = passwordString
|
||||||
|
customDump(envVars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Sources/DewPointController/RunCommand.swift
Normal file
86
Sources/DewPointController/RunCommand.swift
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import Models
|
||||||
|
import MQTTConnectionService
|
||||||
|
import MQTTManager
|
||||||
|
import MQTTNIO
|
||||||
|
import NIO
|
||||||
|
import PsychrometricClientLive
|
||||||
|
import SensorsService
|
||||||
|
import ServiceLifecycle
|
||||||
|
|
||||||
|
extension Application {
|
||||||
|
/// Run the controller.
|
||||||
|
///
|
||||||
|
struct Run: AsyncParsableCommand {
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: "run",
|
||||||
|
abstract: "Run the controller."
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup
|
||||||
|
var shared: SharedOptions
|
||||||
|
|
||||||
|
mutating func run() async throws {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
let eventloopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||||
|
var logger = Logger(label: "dewpoint-controller")
|
||||||
|
let mqtt = try await setup(eventLoopGroup: eventloopGroup, logger: &logger)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.psychrometricClient = .liveValue
|
||||||
|
$0.mqtt = .live(client: mqtt, logger: logger)
|
||||||
|
} operation: {
|
||||||
|
let mqttConnection = MQTTConnectionService(logger: logger)
|
||||||
|
let sensors = SensorsService(sensors: .live, logger: logger)
|
||||||
|
|
||||||
|
var serviceGroupConfiguration = ServiceGroupConfiguration(
|
||||||
|
services: [
|
||||||
|
mqttConnection,
|
||||||
|
sensors
|
||||||
|
],
|
||||||
|
gracefulShutdownSignals: [.sigterm, .sigint],
|
||||||
|
logger: logger
|
||||||
|
)
|
||||||
|
serviceGroupConfiguration.maximumCancellationDuration = .seconds(5)
|
||||||
|
serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10)
|
||||||
|
|
||||||
|
let serviceGroup = ServiceGroup(configuration: serviceGroupConfiguration)
|
||||||
|
|
||||||
|
logger.info("Starting dewpoint-controller!")
|
||||||
|
try await serviceGroup.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
try await mqtt.shutdown()
|
||||||
|
try await eventloopGroup.shutdownGracefully()
|
||||||
|
} catch {
|
||||||
|
try await eventloopGroup.shutdownGracefully()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setup(
|
||||||
|
eventLoopGroup: MultiThreadedEventLoopGroup,
|
||||||
|
logger: inout Logger
|
||||||
|
) async throws -> MQTTClient {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
|
||||||
|
let environment = try await cliClient.makeEnvVars(shared.envVarsRequest(logger: logger))
|
||||||
|
logger.logLevel = cliClient.logLevel(environment)
|
||||||
|
|
||||||
|
return try cliClient.makeClient(.init(
|
||||||
|
envVars: environment,
|
||||||
|
eventLoopGroup: eventLoopGroup,
|
||||||
|
logger: logger
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
50
Sources/DewPointController/SharedOptions.swift
Normal file
50
Sources/DewPointController/SharedOptions.swift
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import CliClient
|
||||||
|
import Logging
|
||||||
|
import Models
|
||||||
|
import MQTTNIO
|
||||||
|
|
||||||
|
extension Application {
|
||||||
|
|
||||||
|
struct SharedOptions: ParsableArguments {
|
||||||
|
@Option(
|
||||||
|
name: [.short, .customLong("env-file")],
|
||||||
|
help: "A file path to an env file."
|
||||||
|
)
|
||||||
|
var envFile: String?
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: [.short, .customLong("log-level")],
|
||||||
|
help: "Set the logging level."
|
||||||
|
)
|
||||||
|
var logLevel: LogLevelContainer?
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: [.short, .long],
|
||||||
|
help: "Set the MQTT connecition version."
|
||||||
|
)
|
||||||
|
var version: String?
|
||||||
|
|
||||||
|
func envVarsRequest(logger: Logger?) -> CliClient.EnvVarsRequest {
|
||||||
|
.init(envFilePath: envFile, logger: logger, version: version)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A container type for making `Logger.Level` into a type
|
||||||
|
/// that can be parsed as a command line argument. This is
|
||||||
|
/// to suppress warnings vs. having `Logger.Level` adopt the
|
||||||
|
/// protocol.
|
||||||
|
@_spi(Internal)
|
||||||
|
public struct LogLevelContainer: ExpressibleByArgument {
|
||||||
|
public let logLevel: Logger.Level?
|
||||||
|
|
||||||
|
public init?(argument: String) {
|
||||||
|
self.logLevel = .init(rawValue: argument.lowercased())
|
||||||
|
}
|
||||||
|
|
||||||
|
public func callAsFunction() -> Logger.Level? {
|
||||||
|
logLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Sources/DewPointController/Utils.swift
Normal file
10
Sources/DewPointController/Utils.swift
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Models
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension Array where Element == TemperatureAndHumiditySensor {
|
||||||
|
static var live: Self {
|
||||||
|
TemperatureAndHumiditySensor.Location.allCases.map { location in
|
||||||
|
TemperatureAndHumiditySensor(location: location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,14 @@ import MQTTManager
|
|||||||
import ServiceLifecycle
|
import ServiceLifecycle
|
||||||
|
|
||||||
public struct MQTTConnectionService: Service {
|
public struct MQTTConnectionService: Service {
|
||||||
@Dependency(\.mqtt) var mqtt
|
|
||||||
|
|
||||||
private let logger: Logger?
|
private let logger: Logger?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
logger: Logger? = nil
|
logger: Logger? = nil
|
||||||
) {
|
) {
|
||||||
|
var logger = logger
|
||||||
|
logger?[metadataKey: "type"] = "mqtt-connection-service"
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ public struct MQTTConnectionService: Service {
|
|||||||
/// to the MQTT broker and handles graceful shutdown of the
|
/// to the MQTT broker and handles graceful shutdown of the
|
||||||
/// connection.
|
/// connection.
|
||||||
public func run() async throws {
|
public func run() async throws {
|
||||||
|
@Dependency(\.mqtt) var mqtt
|
||||||
try await mqtt.connect()
|
try await mqtt.connect()
|
||||||
|
|
||||||
try await withGracefulShutdownHandler {
|
try await withGracefulShutdownHandler {
|
||||||
@@ -30,11 +32,7 @@ public struct MQTTConnectionService: Service {
|
|||||||
}
|
}
|
||||||
} onGracefulShutdown: {
|
} onGracefulShutdown: {
|
||||||
self.logger?.trace("Received graceful shutdown.")
|
self.logger?.trace("Received graceful shutdown.")
|
||||||
shutdown()
|
mqtt.shutdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func shutdown() {
|
|
||||||
mqtt.shutdown()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ import NIO
|
|||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
|
|
||||||
/// A dependency that is responsible for managing the connection to
|
/// A dependency that is responsible for managing the connection to
|
||||||
/// an MQTT broker.
|
/// an MQTT broker, listen to topics, and publish values back to the
|
||||||
|
/// broker.
|
||||||
var mqtt: MQTTManager {
|
var mqtt: MQTTManager {
|
||||||
get { self[MQTTManager.self] }
|
get { self[MQTTManager.self] }
|
||||||
set { self[MQTTManager.self] = newValue }
|
set { self[MQTTManager.self] = newValue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the interface needed for the ``MQTTConnectionService``.
|
/// Represents the interface needed to connect, listen, and publish to an MQTT broker.
|
||||||
///
|
///
|
||||||
/// See ``MQTTConnectionManagerLive`` module for live implementation.
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct MQTTManager: Sendable {
|
public struct MQTTManager: Sendable {
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ public struct MQTTManager: Sendable {
|
|||||||
public var connect: @Sendable () async throws -> Void
|
public var connect: @Sendable () async throws -> Void
|
||||||
|
|
||||||
/// Create a stream of connection events.
|
/// Create a stream of connection events.
|
||||||
|
///
|
||||||
|
/// - SeeAlso: ``Event``
|
||||||
public var connectionStream: @Sendable () throws -> AsyncStream<Event>
|
public var connectionStream: @Sendable () throws -> AsyncStream<Event>
|
||||||
|
|
||||||
private var _listen: @Sendable ([String], MQTTQoS) async throws -> ListenStream
|
private var _listen: @Sendable ([String], MQTTQoS) async throws -> ListenStream
|
||||||
@@ -38,10 +40,24 @@ public struct MQTTManager: Sendable {
|
|||||||
/// Shutdown the connection to the MQTT broker.
|
/// Shutdown the connection to the MQTT broker.
|
||||||
public var shutdown: @Sendable () -> Void
|
public var shutdown: @Sendable () -> Void
|
||||||
|
|
||||||
/// Perform an operation with the underlying MQTTClient, this can be useful in
|
|
||||||
/// tests, so this module needs imported with `@_spi(Testing) import` to use this method.
|
|
||||||
private var _withClient: @Sendable ((MQTTClient) async throws -> Void) async throws -> Void
|
private var _withClient: @Sendable ((MQTTClient) async throws -> Void) async throws -> Void
|
||||||
|
|
||||||
|
public init(
|
||||||
|
connect: @escaping @Sendable () async throws -> Void,
|
||||||
|
connectionStream: @escaping @Sendable () throws -> AsyncStream<MQTTManager.Event>,
|
||||||
|
listen: @escaping @Sendable ([String], MQTTQoS) async throws -> MQTTManager.ListenStream,
|
||||||
|
publish: @escaping @Sendable (MQTTManager.PublishRequest) async throws -> Void,
|
||||||
|
shutdown: @escaping @Sendable () -> Void,
|
||||||
|
withClient: @escaping @Sendable ((MQTTClient) async throws -> Void) async throws -> Void = { _ in unimplemented() }
|
||||||
|
) {
|
||||||
|
self.connect = connect
|
||||||
|
self.connectionStream = connectionStream
|
||||||
|
self._listen = listen
|
||||||
|
self.publish = publish
|
||||||
|
self.shutdown = shutdown
|
||||||
|
self._withClient = withClient
|
||||||
|
}
|
||||||
|
|
||||||
/// Create an async stream that listens for changes to the given topics.
|
/// Create an async stream that listens for changes to the given topics.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -77,18 +93,20 @@ public struct MQTTManager: Sendable {
|
|||||||
_ payload: ByteBuffer,
|
_ payload: ByteBuffer,
|
||||||
to topicName: String,
|
to topicName: String,
|
||||||
qos: MQTTQoS,
|
qos: MQTTQoS,
|
||||||
retain: Bool = false
|
retain: Bool = false,
|
||||||
|
properties: MQTTProperties = .init()
|
||||||
) async throws {
|
) async throws {
|
||||||
try await publish(.init(
|
try await publish(.init(
|
||||||
topicName: topicName,
|
topicName: topicName,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
qos: qos,
|
qos: qos,
|
||||||
retain: retain
|
retain: retain,
|
||||||
|
properties: properties
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform an operation with the underlying MQTTClient, this can be useful in
|
/// Perform an operation with the underlying MQTTClient, this can be useful in
|
||||||
/// tests, so this module needs imported with `@_spi(Testing) import` to use this method.
|
/// tests, so this module needs imported with `@_spi(Internal) import MQTTManager` to use this method.
|
||||||
@_spi(Internal)
|
@_spi(Internal)
|
||||||
public func withClient(
|
public func withClient(
|
||||||
_ callback: @Sendable (MQTTClient) async throws -> Void
|
_ callback: @Sendable (MQTTClient) async throws -> Void
|
||||||
@@ -98,7 +116,7 @@ public struct MQTTManager: Sendable {
|
|||||||
|
|
||||||
/// Represents connection events that clients can listen for and
|
/// Represents connection events that clients can listen for and
|
||||||
/// react accordingly.
|
/// react accordingly.
|
||||||
public enum Event: Sendable {
|
public enum Event: Equatable, Sendable {
|
||||||
case connected
|
case connected
|
||||||
case disconnected
|
case disconnected
|
||||||
case shuttingDown
|
case shuttingDown
|
||||||
@@ -106,7 +124,7 @@ public struct MQTTManager: Sendable {
|
|||||||
|
|
||||||
/// Represents the parameters required to publish a new value to the
|
/// Represents the parameters required to publish a new value to the
|
||||||
/// MQTT broker.
|
/// MQTT broker.
|
||||||
public struct PublishRequest: Equatable, Sendable {
|
public struct PublishRequest: Sendable {
|
||||||
|
|
||||||
/// The topic to publish the new value to.
|
/// The topic to publish the new value to.
|
||||||
public let topicName: String
|
public let topicName: String
|
||||||
@@ -120,6 +138,8 @@ public struct MQTTManager: Sendable {
|
|||||||
/// The retain flag for the request.
|
/// The retain flag for the request.
|
||||||
public let retain: Bool
|
public let retain: Bool
|
||||||
|
|
||||||
|
public let properties: MQTTProperties
|
||||||
|
|
||||||
/// Create a new publish request.
|
/// Create a new publish request.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -131,12 +151,14 @@ public struct MQTTManager: Sendable {
|
|||||||
topicName: String,
|
topicName: String,
|
||||||
payload: ByteBuffer,
|
payload: ByteBuffer,
|
||||||
qos: MQTTQoS,
|
qos: MQTTQoS,
|
||||||
retain: Bool
|
retain: Bool,
|
||||||
|
properties: MQTTProperties
|
||||||
) {
|
) {
|
||||||
self.topicName = topicName
|
self.topicName = topicName
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.qos = qos
|
self.qos = qos
|
||||||
self.retain = retain
|
self.retain = retain
|
||||||
|
self.properties = properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +186,7 @@ public extension MQTTManager {
|
|||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.eraseToStream()
|
.eraseToStream()
|
||||||
},
|
},
|
||||||
_listen: { topics, qos in
|
listen: { topics, qos in
|
||||||
try await manager.listen(to: topics, qos: qos)
|
try await manager.listen(to: topics, qos: qos)
|
||||||
},
|
},
|
||||||
publish: { request in
|
publish: { request in
|
||||||
@@ -174,19 +196,20 @@ public extension MQTTManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger?.trace("Begin publishing to topic: \(topic)")
|
logger?.trace("Begin publishing to topic: \(topic)")
|
||||||
defer { logger?.trace("Done publishing to topic: \(topic)") }
|
defer { logger?.debug("Done publishing to topic: \(topic)") }
|
||||||
try await client.publish(
|
try await client.publish(
|
||||||
to: request.topicName,
|
to: request.topicName,
|
||||||
payload: request.payload,
|
payload: request.payload,
|
||||||
qos: request.qos,
|
qos: request.qos,
|
||||||
retain: request.retain
|
retain: request.retain,
|
||||||
)
|
properties: request.properties
|
||||||
|
).get()
|
||||||
},
|
},
|
||||||
shutdown: {
|
shutdown: {
|
||||||
Task { try await client.shutdown() }
|
Task { try await client.shutdown() }
|
||||||
manager.shutdown()
|
manager.shutdown()
|
||||||
},
|
},
|
||||||
_withClient: { callback in
|
withClient: { callback in
|
||||||
try await callback(client)
|
try await callback(client)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -107,42 +107,6 @@ actor TopicListenerStream {
|
|||||||
onShutdownHandler = { task.cancel() }
|
onShutdownHandler = { task.cancel() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove.
|
|
||||||
func listen(
|
|
||||||
_ topics: [String],
|
|
||||||
_ qos: MQTTQoS = .atLeastOnce
|
|
||||||
) async throws -> Stream {
|
|
||||||
var sleepTimes = 0
|
|
||||||
|
|
||||||
while !client.isActive() {
|
|
||||||
guard sleepTimes < 10 else {
|
|
||||||
throw TopicListenerError.connectionTimeout
|
|
||||||
}
|
|
||||||
try? await Task.sleep(for: .milliseconds(100))
|
|
||||||
sleepTimes += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
client.logger.trace("Client is active, begin subscribing to topics.")
|
|
||||||
|
|
||||||
try await subscribe()
|
|
||||||
|
|
||||||
client.logger.trace("Done subscribing, begin listening to topics.")
|
|
||||||
|
|
||||||
client.addPublishListener(named: name) { result in
|
|
||||||
switch result {
|
|
||||||
case let .failure(error):
|
|
||||||
self.logger?.error("Received error while listening: \(error)")
|
|
||||||
case let .success(publishInfo):
|
|
||||||
if topics.contains(publishInfo.topicName) {
|
|
||||||
self.logger?.debug("Recieved new value for topic: \(publishInfo.topicName)")
|
|
||||||
self.continuation.yield(publishInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setIsShuttingDown() {
|
private func setIsShuttingDown() {
|
||||||
shuttingDown = true
|
shuttingDown = true
|
||||||
onShutdownHandler = nil
|
onShutdownHandler = nil
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
|||||||
/// The MQTT user password.
|
/// The MQTT user password.
|
||||||
public var password: String?
|
public var password: String?
|
||||||
|
|
||||||
|
/// Set a custom logging level.
|
||||||
|
public var logLevel: Logger.Level?
|
||||||
|
|
||||||
|
/// Set the mqtt broker version.
|
||||||
|
public var version: String?
|
||||||
|
|
||||||
/// Create a new ``EnvVars``
|
/// Create a new ``EnvVars``
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -38,9 +44,11 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
|||||||
appEnv: AppEnv = .development,
|
appEnv: AppEnv = .development,
|
||||||
host: String = "127.0.0.1",
|
host: String = "127.0.0.1",
|
||||||
port: String? = "1883",
|
port: String? = "1883",
|
||||||
identifier: String = "dewPoint-controller",
|
identifier: String = "dewpoint-controller",
|
||||||
userName: String? = "mqtt_user",
|
userName: String? = "mqtt_user",
|
||||||
password: String? = "secret!"
|
password: String? = "secret!",
|
||||||
|
logLevel: Logger.Level? = nil,
|
||||||
|
version: String? = nil
|
||||||
) {
|
) {
|
||||||
self.appEnv = appEnv
|
self.appEnv = appEnv
|
||||||
self.host = host
|
self.host = host
|
||||||
@@ -48,6 +56,8 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
|||||||
self.identifier = identifier
|
self.identifier = identifier
|
||||||
self.userName = userName
|
self.userName = userName
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.logLevel = logLevel
|
||||||
|
self.version = version
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom coding keys.
|
/// Custom coding keys.
|
||||||
@@ -58,6 +68,8 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
|||||||
case identifier = "MQTT_IDENTIFIER"
|
case identifier = "MQTT_IDENTIFIER"
|
||||||
case userName = "MQTT_USERNAME"
|
case userName = "MQTT_USERNAME"
|
||||||
case password = "MQTT_PASSWORD"
|
case password = "MQTT_PASSWORD"
|
||||||
|
case logLevel = "LOG_LEVEL"
|
||||||
|
case version = "MQTT_VERSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the different app environments.
|
/// Represents the different app environments.
|
||||||
|
|||||||
83
Tests/SensorsServiceTests/SensorsClientTests.swift → Tests/IntegrationTests/IntegrationTests.swift
Executable file → Normal file
83
Tests/SensorsServiceTests/SensorsClientTests.swift → Tests/IntegrationTests/IntegrationTests.swift
Executable file → Normal file
@@ -1,20 +1,25 @@
|
|||||||
import Dependencies
|
import Dependencies
|
||||||
|
|
||||||
|
// @_spi(Internal) import dewpoint_controller
|
||||||
import Logging
|
import Logging
|
||||||
import Models
|
import Models
|
||||||
|
import MQTTConnectionService
|
||||||
@_spi(Internal) import MQTTManager
|
@_spi(Internal) import MQTTManager
|
||||||
import MQTTNIO
|
import MQTTNIO
|
||||||
import NIO
|
import NIO
|
||||||
import PsychrometricClientLive
|
import PsychrometricClientLive
|
||||||
@_spi(Internal) import SensorsService
|
@_spi(Internal) import SensorsService
|
||||||
|
import ServiceLifecycle
|
||||||
|
import ServiceLifecycleTestKit
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class SensorsClientTests: XCTestCase {
|
final class IntegrationTests: XCTestCase {
|
||||||
|
|
||||||
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
|
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
|
||||||
|
|
||||||
static let logger: Logger = {
|
static let logger: Logger = {
|
||||||
var logger = Logger(label: "SensorsClientTests")
|
var logger = Logger(label: "IntegrationTests")
|
||||||
logger.logLevel = .trace
|
logger.logLevel = .info
|
||||||
return logger
|
return logger
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -29,6 +34,71 @@ final class SensorsClientTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testConnectionServiceShutdown() async throws {
|
||||||
|
@Dependency(\.mqtt) var mqtt
|
||||||
|
|
||||||
|
let service = MQTTConnectionService(logger: Self.logger)
|
||||||
|
let task = Task { try await service.run() }
|
||||||
|
defer { task.cancel() }
|
||||||
|
|
||||||
|
try await Task.sleep(for: .milliseconds(200))
|
||||||
|
|
||||||
|
// check the connection is active here.
|
||||||
|
try await mqtt.withClient { client in
|
||||||
|
XCTAssert(client.isActive())
|
||||||
|
}
|
||||||
|
mqtt.shutdown()
|
||||||
|
|
||||||
|
try await Task.sleep(for: .milliseconds(500))
|
||||||
|
|
||||||
|
// check the connection is active here.
|
||||||
|
try await mqtt.withClient { client in
|
||||||
|
XCTAssertFalse(client.isActive())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMQTTConnectionStream() async throws {
|
||||||
|
let client = createClient(identifier: "testNonManagedStream")
|
||||||
|
let manager = MQTTManager.live(
|
||||||
|
client: client,
|
||||||
|
logger: Self.logger,
|
||||||
|
alwaysReconnect: false
|
||||||
|
)
|
||||||
|
defer { manager.shutdown() }
|
||||||
|
let connectionStream1 = MQTTConnectionStream(client: client, logger: Self.logger)
|
||||||
|
let connectionStream2 = MQTTConnectionStream(client: client, logger: Self.logger)
|
||||||
|
var events1 = [MQTTManager.Event]()
|
||||||
|
var events2 = [MQTTManager.Event]()
|
||||||
|
|
||||||
|
let stream1 = connectionStream1.start()
|
||||||
|
let stream2 = connectionStream2.start()
|
||||||
|
|
||||||
|
_ = try await manager.connect()
|
||||||
|
|
||||||
|
Task {
|
||||||
|
while !client.isActive() {
|
||||||
|
try await Task.sleep(for: .milliseconds(100))
|
||||||
|
}
|
||||||
|
try await Task.sleep(for: .milliseconds(200))
|
||||||
|
try await client.disconnect()
|
||||||
|
try await Task.sleep(for: .milliseconds(500))
|
||||||
|
manager.shutdown()
|
||||||
|
try await Task.sleep(for: .milliseconds(500))
|
||||||
|
connectionStream1.stop()
|
||||||
|
connectionStream2.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
for await event in stream1.removeDuplicates() {
|
||||||
|
events1.append(event)
|
||||||
|
}
|
||||||
|
for await event in stream2.removeDuplicates() {
|
||||||
|
events2.append(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(events1, [.disconnected, .connected, .disconnected, .shuttingDown])
|
||||||
|
XCTAssertEqual(events2, [.disconnected, .connected, .disconnected, .shuttingDown])
|
||||||
|
}
|
||||||
|
|
||||||
func testListeningResumesAfterDisconnectThenReconnect() async throws {
|
func testListeningResumesAfterDisconnectThenReconnect() async throws {
|
||||||
struct TimeoutError: Error {}
|
struct TimeoutError: Error {}
|
||||||
|
|
||||||
@@ -99,6 +169,8 @@ final class SensorsClientTests: XCTestCase {
|
|||||||
userName: nil,
|
userName: nil,
|
||||||
password: nil
|
password: nil
|
||||||
)
|
)
|
||||||
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||||
|
// return .init(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: Self.logger)
|
||||||
let config = MQTTClient.Configuration(
|
let config = MQTTClient.Configuration(
|
||||||
version: .v3_1_1,
|
version: .v3_1_1,
|
||||||
userName: envVars.userName,
|
userName: envVars.userName,
|
||||||
@@ -111,14 +183,15 @@ final class SensorsClientTests: XCTestCase {
|
|||||||
return .init(
|
return .init(
|
||||||
host: Self.hostname,
|
host: Self.hostname,
|
||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1)),
|
eventLoopGroupProvider: .shared(eventLoopGroup),
|
||||||
logger: Self.logger,
|
logger: Self.logger,
|
||||||
configuration: config
|
configuration: config
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Helpers for tests.
|
// - MARK: Helpers
|
||||||
|
|
||||||
struct TopicNotFoundError: Error {}
|
struct TopicNotFoundError: Error {}
|
||||||
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import Dependencies
|
|
||||||
import Logging
|
|
||||||
import Models
|
|
||||||
import MQTTConnectionService
|
|
||||||
@_spi(Internal) import MQTTManager
|
|
||||||
import MQTTNIO
|
|
||||||
import NIO
|
|
||||||
import PsychrometricClientLive
|
|
||||||
import ServiceLifecycle
|
|
||||||
import ServiceLifecycleTestKit
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
// TODO: Rename to integration test when other tests are moved.
|
|
||||||
final class MQTTConnectionServiceTests: XCTestCase {
|
|
||||||
|
|
||||||
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
|
|
||||||
|
|
||||||
static let logger: Logger = {
|
|
||||||
var logger = Logger(label: "MQTTConnectionServiceTests")
|
|
||||||
logger.logLevel = .info
|
|
||||||
return logger
|
|
||||||
}()
|
|
||||||
|
|
||||||
override func invokeTest() {
|
|
||||||
let client = createClient(identifier: "\(Self.self)")
|
|
||||||
|
|
||||||
withDependencies {
|
|
||||||
$0.mqtt = .live(client: client, logger: Self.logger)
|
|
||||||
$0.psychrometricClient = PsychrometricClient.liveValue
|
|
||||||
} operation: {
|
|
||||||
super.invokeTest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testConnectionServiceShutdown() async throws {
|
|
||||||
@Dependency(\.mqtt) var mqtt
|
|
||||||
|
|
||||||
let service = MQTTConnectionService(logger: Self.logger)
|
|
||||||
let task = Task { try await service.run() }
|
|
||||||
defer { task.cancel() }
|
|
||||||
|
|
||||||
try await Task.sleep(for: .milliseconds(200))
|
|
||||||
|
|
||||||
// check the connection is active here.
|
|
||||||
try await mqtt.withClient { client in
|
|
||||||
XCTAssert(client.isActive())
|
|
||||||
}
|
|
||||||
service.shutdown()
|
|
||||||
|
|
||||||
try await Task.sleep(for: .milliseconds(500))
|
|
||||||
|
|
||||||
// check the connection is active here.
|
|
||||||
try await mqtt.withClient { client in
|
|
||||||
XCTAssertFalse(client.isActive())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Move to integration tests.
|
|
||||||
func testMQTTConnectionStream() async throws {
|
|
||||||
let client = createClient(identifier: "testNonManagedStream")
|
|
||||||
let manager = MQTTManager.live(
|
|
||||||
client: client,
|
|
||||||
logger: Self.logger,
|
|
||||||
alwaysReconnect: false
|
|
||||||
)
|
|
||||||
defer { manager.shutdown() }
|
|
||||||
let connectionStream1 = MQTTConnectionStream(client: client, logger: Self.logger)
|
|
||||||
let connectionStream2 = MQTTConnectionStream(client: client, logger: Self.logger)
|
|
||||||
var events1 = [MQTTManager.Event]()
|
|
||||||
var events2 = [MQTTManager.Event]()
|
|
||||||
|
|
||||||
let stream1 = connectionStream1.start()
|
|
||||||
let stream2 = connectionStream2.start()
|
|
||||||
|
|
||||||
_ = try await manager.connect()
|
|
||||||
|
|
||||||
Task {
|
|
||||||
while !client.isActive() {
|
|
||||||
try await Task.sleep(for: .milliseconds(100))
|
|
||||||
}
|
|
||||||
try await Task.sleep(for: .milliseconds(200))
|
|
||||||
try await client.disconnect()
|
|
||||||
try await Task.sleep(for: .milliseconds(500))
|
|
||||||
manager.shutdown()
|
|
||||||
try await Task.sleep(for: .milliseconds(500))
|
|
||||||
connectionStream1.stop()
|
|
||||||
connectionStream2.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
for await event in stream1.removeDuplicates() {
|
|
||||||
events1.append(event)
|
|
||||||
}
|
|
||||||
for await event in stream2.removeDuplicates() {
|
|
||||||
events2.append(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
XCTAssertEqual(events1, [.disconnected, .connected, .disconnected, .shuttingDown])
|
|
||||||
XCTAssertEqual(events2, [.disconnected, .connected, .disconnected, .shuttingDown])
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClient(identifier: String) -> MQTTClient {
|
|
||||||
let envVars = EnvVars(
|
|
||||||
appEnv: .testing,
|
|
||||||
host: Self.hostname,
|
|
||||||
port: "1883",
|
|
||||||
identifier: identifier,
|
|
||||||
userName: nil,
|
|
||||||
password: nil
|
|
||||||
)
|
|
||||||
let config = MQTTClient.Configuration(
|
|
||||||
version: .v3_1_1,
|
|
||||||
userName: envVars.userName,
|
|
||||||
password: envVars.password,
|
|
||||||
useSSL: false,
|
|
||||||
useWebSockets: false,
|
|
||||||
tlsConfiguration: nil,
|
|
||||||
webSocketURLPath: nil
|
|
||||||
)
|
|
||||||
return .init(
|
|
||||||
host: Self.hostname,
|
|
||||||
identifier: identifier,
|
|
||||||
eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1)),
|
|
||||||
logger: Self.logger,
|
|
||||||
configuration: config
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# Used this to build the release version of the image.
|
# Used this to build the release version of the image.
|
||||||
# Build the executable
|
# Build the executable
|
||||||
FROM swift:5.10 AS build
|
ARG SWIFT_IMAGE_VERSION="5.10"
|
||||||
|
|
||||||
|
FROM swift:${SWIFT_IMAGE_VERSION} AS build
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY ./Package.* ./
|
COPY ./Package.* ./
|
||||||
RUN swift package resolve
|
RUN swift package resolve
|
||||||
@@ -8,7 +10,6 @@ COPY . .
|
|||||||
RUN swift build -c release -Xswiftc -g
|
RUN swift build -c release -Xswiftc -g
|
||||||
|
|
||||||
# Run image
|
# Run image
|
||||||
FROM swift:5.10-slim
|
FROM swift:${SWIFT_IMAGE_VERSION}-slim
|
||||||
WORKDIR /run
|
COPY --from=build /build/.build/release/dewpoint-controller /usr/local/bin
|
||||||
COPY --from=build /build/.build/release/dewpoint-controller /run
|
CMD ["/bin/bash", "-xc", "/usr/local/bin/dewpoint-controller run"]
|
||||||
CMD ["/bin/bash", "-xc", "./dewpoint-controller"]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user