Working on tests.
This commit is contained in:
11
Makefile
11
Makefile
@@ -10,3 +10,14 @@ build:
|
|||||||
|
|
||||||
run:
|
run:
|
||||||
@swift run dewPoint-controller
|
@swift run dewPoint-controller
|
||||||
|
|
||||||
|
start-mosquitto:
|
||||||
|
@docker run \
|
||||||
|
--name mosquitto \
|
||||||
|
-d \
|
||||||
|
-p 1883:1883 \
|
||||||
|
-v $(PWD)/mosquitto/config:/mosquitto/config \
|
||||||
|
eclipse-mosquitto
|
||||||
|
|
||||||
|
stop-mosquitto:
|
||||||
|
@docker rm -f mosquitto || true
|
||||||
|
|||||||
@@ -80,5 +80,12 @@ let package = Package(
|
|||||||
.product(name: "MQTTNIO", package: "mqtt-nio")
|
.product(name: "MQTTNIO", package: "mqtt-nio")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ClientTests",
|
||||||
|
dependencies: [
|
||||||
|
"Client",
|
||||||
|
"ClientLive"
|
||||||
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ extension EventLoopFuture where Value == (EnvVars, Topics) {
|
|||||||
map { envVars, topics in
|
map { envVars, topics in
|
||||||
let nioClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger)
|
let nioClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger)
|
||||||
return DewPointEnvironment.init(
|
return DewPointEnvironment.init(
|
||||||
mqttClient: .live(client: nioClient),
|
mqttClient: .live(client: nioClient, topics: topics),
|
||||||
envVars: envVars,
|
envVars: envVars,
|
||||||
nioClient: nioClient,
|
nioClient: nioClient,
|
||||||
topics: topics
|
topics: topics
|
||||||
@@ -148,7 +148,7 @@ extension EventLoopFuture where Value == DewPointEnvironment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MQTTClient {
|
extension MQTTNIO.MQTTClient {
|
||||||
|
|
||||||
fileprivate convenience init(envVars: EnvVars, eventLoopGroup: EventLoopGroup, logger: Logger?) {
|
fileprivate convenience init(envVars: EnvVars, eventLoopGroup: EventLoopGroup, logger: Logger?) {
|
||||||
self.init(
|
self.init(
|
||||||
|
|||||||
@@ -9,15 +9,18 @@ import Psychrometrics
|
|||||||
///
|
///
|
||||||
/// This is an abstraction around the ``MQTTNIO.MQTTClient``.
|
/// This is an abstraction around the ``MQTTNIO.MQTTClient``.
|
||||||
public struct MQTTClient {
|
public struct MQTTClient {
|
||||||
|
|
||||||
/// Retrieve the humidity from the MQTT Broker.
|
/// Retrieve the humidity from the MQTT Broker.
|
||||||
public var fetchHumidity: (Sensor<RelativeHumidity>) -> EventLoopFuture<RelativeHumidity>
|
public var fetchHumidity: (Sensor<RelativeHumidity>) -> EventLoopFuture<RelativeHumidity>
|
||||||
|
|
||||||
|
/// Retrieve a set point from the MQTT Broker.
|
||||||
|
public var fetchSetPoint: (KeyPath<Topics.SetPoints, String>) -> EventLoopFuture<Double>
|
||||||
|
|
||||||
/// Retrieve the temperature from the MQTT Broker.
|
/// Retrieve the temperature from the MQTT Broker.
|
||||||
public var fetchTemperature: (Sensor<Temperature>, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
|
public var fetchTemperature: (Sensor<Temperature>, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
|
||||||
|
|
||||||
/// Publish a change of state message for a relay.
|
/// Publish a change of state message for a relay.
|
||||||
public var setRelay: (Relay, Relay.State) -> EventLoopFuture<Void>
|
public var setRelay: (KeyPath<Topics.Commands.Relays, String>, Relay.State) -> EventLoopFuture<Void>
|
||||||
|
|
||||||
/// Disconnect and close the connection to the MQTT Broker.
|
/// Disconnect and close the connection to the MQTT Broker.
|
||||||
public var shutdown: () -> EventLoopFuture<Void>
|
public var shutdown: () -> EventLoopFuture<Void>
|
||||||
@@ -27,12 +30,14 @@ public struct MQTTClient {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
fetchHumidity: @escaping (Sensor<RelativeHumidity>) -> EventLoopFuture<RelativeHumidity>,
|
fetchHumidity: @escaping (Sensor<RelativeHumidity>) -> EventLoopFuture<RelativeHumidity>,
|
||||||
|
fetchSetPoint: @escaping (KeyPath<Topics.SetPoints, String>) -> EventLoopFuture<Double>,
|
||||||
fetchTemperature: @escaping (Sensor<Temperature>, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>,
|
fetchTemperature: @escaping (Sensor<Temperature>, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>,
|
||||||
setRelay: @escaping (Relay, Relay.State) -> EventLoopFuture<Void>,
|
setRelay: @escaping (KeyPath<Topics.Commands.Relays, String>, Relay.State) -> EventLoopFuture<Void>,
|
||||||
shutdown: @escaping () -> EventLoopFuture<Void>,
|
shutdown: @escaping () -> EventLoopFuture<Void>,
|
||||||
publishDewPoint: @escaping (DewPoint, String) -> EventLoopFuture<Void>
|
publishDewPoint: @escaping (DewPoint, String) -> EventLoopFuture<Void>
|
||||||
) {
|
) {
|
||||||
self.fetchHumidity = fetchHumidity
|
self.fetchHumidity = fetchHumidity
|
||||||
|
self.fetchSetPoint = fetchSetPoint
|
||||||
self.fetchTemperature = fetchTemperature
|
self.fetchTemperature = fetchTemperature
|
||||||
self.setRelay = setRelay
|
self.setRelay = setRelay
|
||||||
self.shutdown = shutdown
|
self.shutdown = shutdown
|
||||||
@@ -60,7 +65,7 @@ public struct MQTTClient {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - relay: The relay to send the message to.
|
/// - relay: The relay to send the message to.
|
||||||
/// - state: The state to change the relay to.
|
/// - state: The state to change the relay to.
|
||||||
public func `set`(relay: Relay, to state: Relay.State) -> EventLoopFuture<Void> {
|
public func `set`(relay: KeyPath<Topics.Commands.Relays, String>, to state: Relay.State) -> EventLoopFuture<Void> {
|
||||||
setRelay(relay, state)
|
setRelay(relay, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,55 @@ enum MQTTError: Error {
|
|||||||
case relay(reason: String, error: Error?)
|
case relay(reason: String, error: Error?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol FetchableTopic {
|
||||||
|
associatedtype Value: BufferInitalizable
|
||||||
|
var topic: String { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Double: BufferInitalizable {
|
||||||
|
|
||||||
|
init?(buffer: inout ByteBuffer) {
|
||||||
|
guard let string = buffer.readString(length: buffer.readableBytes) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.init(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//extension SetPoint: FetchableTopic {
|
||||||
|
// typealias Value = Double
|
||||||
|
//}
|
||||||
|
|
||||||
|
extension Sensor: FetchableTopic where Reading: BufferInitalizable {
|
||||||
|
typealias Value = Reading
|
||||||
|
}
|
||||||
|
|
||||||
extension MQTTNIO.MQTTClient {
|
extension MQTTNIO.MQTTClient {
|
||||||
|
|
||||||
|
func mqttSubscription(topic: String, qos: MQTTQoS = .atLeastOnce, retainAsPublished: Bool = true, retainHandling: MQTTSubscribeInfoV5.RetainHandling = .sendAlways) -> MQTTSubscribeInfoV5 {
|
||||||
|
.init(topicFilter: topic, qos: qos, retainAsPublished: retainAsPublished, retainHandling: retainHandling)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch<Value>(
|
||||||
|
_ subscription: MQTTSubscribeInfoV5
|
||||||
|
) -> EventLoopFuture<Value> where Value: BufferInitalizable {
|
||||||
|
logger.debug("Fetching data for: \(subscription.topicFilter)")
|
||||||
|
return v5.subscribe(to: [subscription])
|
||||||
|
.flatMap { _ in
|
||||||
|
let promise = self.eventLoopGroup.next().makePromise(of: Value.self)
|
||||||
|
self.addPublishListener(named: subscription.topicFilter + "-listener") { result in
|
||||||
|
|
||||||
|
result.mapBuffer(to: Value.self)
|
||||||
|
.unwrap(or: MQTTError.sensor(reason: "Invalid sensor reading", error: nil))
|
||||||
|
.fullfill(promise: promise)
|
||||||
|
|
||||||
|
self.logger.debug("Done fetching data for: \(subscription.topicFilter)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.futureResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch a sensor state and convert it appropriately, when the sensor type is ``BufferInitializable``.
|
/// Fetch a sensor state and convert it appropriately, when the sensor type is ``BufferInitializable``.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -52,32 +99,17 @@ extension MQTTNIO.MQTTClient {
|
|||||||
func fetch<S>(
|
func fetch<S>(
|
||||||
sensor: Sensor<S>
|
sensor: Sensor<S>
|
||||||
) -> EventLoopFuture<S> where S: BufferInitalizable {
|
) -> EventLoopFuture<S> where S: BufferInitalizable {
|
||||||
logger.debug("Fetching data for sensor: \(sensor.topic)")
|
return fetch(mqttSubscription(topic: sensor.topic))
|
||||||
let subscription = MQTTSubscribeInfoV5.init(
|
|
||||||
topicFilter: sensor.topic,
|
|
||||||
qos: .atLeastOnce,
|
|
||||||
retainAsPublished: false,
|
|
||||||
retainHandling: .sendAlways
|
|
||||||
)
|
|
||||||
return v5.subscribe(to: [subscription])
|
|
||||||
.flatMap { _ in
|
|
||||||
let promise = self.eventLoopGroup.next().makePromise(of: S.self)
|
|
||||||
self.addPublishListener(named: sensor.topic) { result in
|
|
||||||
|
|
||||||
result.mapBuffer(to: S.self)
|
|
||||||
.unwrap(or: MQTTError.sensor(reason: "Invalid sensor reading", error: nil))
|
|
||||||
.fullfill(promise: promise)
|
|
||||||
|
|
||||||
self.logger.debug("Done fetching data for sensor: \(sensor.topic)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.futureResult
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func `set`(relay: Relay, to state: Relay.State, qos: MQTTQoS = .atLeastOnce) -> EventLoopFuture<Void> {
|
func fetch(setPoint: KeyPath<Topics.SetPoints, String>, setPoints: Topics.SetPoints) -> EventLoopFuture<Double> {
|
||||||
|
// logger.debug("Fetching data for set point: \(setPoint.topic)")
|
||||||
|
return fetch(mqttSubscription(topic: setPoints[keyPath: setPoint]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func `set`(relay relayTopic: String, to state: Relay.State, qos: MQTTQoS = .atLeastOnce) -> EventLoopFuture<Void> {
|
||||||
publish(
|
publish(
|
||||||
to: relay.topic,
|
to: relayTopic,
|
||||||
payload: ByteBufferAllocator().buffer(string: state.rawValue),
|
payload: ByteBufferAllocator().buffer(string: state.rawValue),
|
||||||
qos: qos
|
qos: qos
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Client
|
@_exported import Client
|
||||||
import CoreUnitTypes
|
import CoreUnitTypes
|
||||||
import Models
|
import Models
|
||||||
import MQTTNIO
|
import MQTTNIO
|
||||||
@@ -11,20 +11,24 @@ extension Client.MQTTClient {
|
|||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - client: The ``MQTTNIO.MQTTClient`` used to send and recieve messages from the MQTT Broker.
|
/// - client: The ``MQTTNIO.MQTTClient`` used to send and recieve messages from the MQTT Broker.
|
||||||
public static func live(client: MQTTNIO.MQTTClient) -> Self {
|
public static func live(client: MQTTNIO.MQTTClient, topics: Topics) -> Self {
|
||||||
.init(
|
.init(
|
||||||
fetchHumidity: { sensor in
|
fetchHumidity: { sensor in
|
||||||
client.fetch(sensor: sensor)
|
client.fetch(sensor: sensor)
|
||||||
.debug(logger: client.logger)
|
.debug(logger: client.logger)
|
||||||
},
|
},
|
||||||
|
fetchSetPoint: { setPointKeyPath in
|
||||||
|
client.fetch(client.mqttSubscription(topic: topics.setPoints[keyPath: setPointKeyPath]))
|
||||||
|
.debug(logger: client.logger)
|
||||||
|
},
|
||||||
fetchTemperature: { sensor, units in
|
fetchTemperature: { sensor, units in
|
||||||
client.fetch(sensor: sensor)
|
client.fetch(sensor: sensor)
|
||||||
.debug(logger: client.logger)
|
.debug(logger: client.logger)
|
||||||
.convertIfNeeded(to: units)
|
.convertIfNeeded(to: units)
|
||||||
.debug(logger: client.logger)
|
.debug(logger: client.logger)
|
||||||
},
|
},
|
||||||
setRelay: { relay, state in
|
setRelay: { relayKeyPath, state in
|
||||||
client.set(relay: relay, to: state)
|
client.set(relay: topics.commands.relays[keyPath: relayKeyPath], to: state)
|
||||||
},
|
},
|
||||||
shutdown: {
|
shutdown: {
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ public struct Topics: Codable, Equatable {
|
|||||||
/// The command topics the application can publish to.
|
/// The command topics the application can publish to.
|
||||||
public var commands: Commands
|
public var commands: Commands
|
||||||
|
|
||||||
/// The sensor topics the application can read sensor values from.
|
/// The sensor topics the application can read from / write to.
|
||||||
public var sensors: Sensors
|
public var sensors: Sensors
|
||||||
|
|
||||||
/// The set point topics the application can read set point values from.
|
/// The set point topics the application can read set point values from.
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ defer {
|
|||||||
while true {
|
while true {
|
||||||
// let temp = try environment.mqttClient.fetchTemperature(tempSensor, .imperial).wait()
|
// let temp = try environment.mqttClient.fetchTemperature(tempSensor, .imperial).wait()
|
||||||
// logger.debug("Temp: \(temp.rawValue)")
|
// logger.debug("Temp: \(temp.rawValue)")
|
||||||
|
//
|
||||||
|
// logger.debug("Fetching set-point...")
|
||||||
|
// let sp = try environment.mqttClient.fetchSetPoint(\.dehumidify.highDewPoint).wait()
|
||||||
|
// logger.debug("Set point: \(sp)")
|
||||||
|
//
|
||||||
logger.debug("Fetching dew point...")
|
logger.debug("Fetching dew point...")
|
||||||
|
|
||||||
let dp = try environment.mqttClient.currentDewPoint(
|
let dp = try environment.mqttClient.currentDewPoint(
|
||||||
|
|||||||
78
Tests/ClientTests/ClientTests.swift
Normal file
78
Tests/ClientTests/ClientTests.swift
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import Client
|
||||||
|
@testable import ClientLive
|
||||||
|
import CoreUnitTypes
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import Models
|
||||||
|
import MQTTNIO
|
||||||
|
import NIO
|
||||||
|
import NIOConcurrencyHelpers
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
// Can't seem to get tests to work, although we get values when ran from command line.
|
||||||
|
final class ClientLiveTests: XCTestCase {
|
||||||
|
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
|
||||||
|
let topics = Topics()
|
||||||
|
|
||||||
|
// func test_fetch_humidity() throws {
|
||||||
|
// let lock = Lock()
|
||||||
|
// let mqttClient = createMQTTClient(identifier: "fetchHumidity")
|
||||||
|
//
|
||||||
|
//// let exp = XCTestExpectation(description: "fetchHumidity")
|
||||||
|
//
|
||||||
|
// let client = try createClient(mqttClient: mqttClient)
|
||||||
|
// var humidityRecieved: [RelativeHumidity] = []
|
||||||
|
//
|
||||||
|
// _ = try mqttClient.publish(
|
||||||
|
// to: topics.sensors.humidity,
|
||||||
|
// payload: ByteBufferAllocator().buffer(string: "\(50.0)"),
|
||||||
|
// qos: .atLeastOnce
|
||||||
|
// ).wait()
|
||||||
|
//
|
||||||
|
// Thread.sleep(forTimeInterval: 2)
|
||||||
|
//
|
||||||
|
//// .flatMapThrowing { _ in
|
||||||
|
// let humidity = try client.fetchHumidity(.init(topic: self.topics.sensors.humidity)).wait()
|
||||||
|
// XCTAssertEqual(humidity, 50)
|
||||||
|
// lock.withLock {
|
||||||
|
// humidityRecieved.append(humidity)
|
||||||
|
// }
|
||||||
|
//// exp.fulfill()
|
||||||
|
//// }.wait()
|
||||||
|
//
|
||||||
|
// Thread.sleep(forTimeInterval: 2)
|
||||||
|
// lock.withLock {
|
||||||
|
// XCTAssertEqual(humidityRecieved.count, 1)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// try mqttClient.disconnect().wait()
|
||||||
|
// try mqttClient.syncShutdownGracefully()
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
func createMQTTClient(identifier: String) -> MQTTNIO.MQTTClient {
|
||||||
|
MQTTNIO.MQTTClient(
|
||||||
|
host: Self.hostname,
|
||||||
|
port: 1883,
|
||||||
|
identifier: identifier,
|
||||||
|
eventLoopGroupProvider: .createNew,
|
||||||
|
logger: self.logger,
|
||||||
|
configuration: .init(version: .v5_0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uses default topic names.
|
||||||
|
func createClient(mqttClient: MQTTNIO.MQTTClient, autoConnect: Bool = true) throws -> Client.MQTTClient {
|
||||||
|
if autoConnect {
|
||||||
|
_ = try mqttClient.connect().wait()
|
||||||
|
}
|
||||||
|
return .live(client: mqttClient, topics: .init())
|
||||||
|
}
|
||||||
|
|
||||||
|
let logger: Logger = {
|
||||||
|
var logger = Logger(label: "MQTTTests")
|
||||||
|
logger.logLevel = .trace
|
||||||
|
return logger
|
||||||
|
}()
|
||||||
|
}
|
||||||
22
docker-compose.yaml
Normal file
22
docker-compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# run this with docker-compose -f docker/docker-compose.yml run test
|
||||||
|
version: "3.3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: swift:5.3
|
||||||
|
working_dir: /dewPoint-controller
|
||||||
|
volumes:
|
||||||
|
- .:/dewPoint-controller
|
||||||
|
depends_on:
|
||||||
|
- mosquitto
|
||||||
|
environment:
|
||||||
|
- MOSQUITTO_SERVER=mosquitto
|
||||||
|
command: /bin/bash -xcl "swift test --enable-test-discovery --sanitize=thread"
|
||||||
|
|
||||||
|
mosquitto:
|
||||||
|
image: eclipse-mosquitto
|
||||||
|
volumes:
|
||||||
|
- ./mosquitto/config:/mosquitto/config
|
||||||
|
- ./mosquitto/certs:/mosquitto/certs
|
||||||
|
ports:
|
||||||
|
- "1883:1883"
|
||||||
10
mosquitto/config/mosquitto.conf
Normal file
10
mosquitto/config/mosquitto.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Setup
|
||||||
|
allow_anonymous true
|
||||||
|
allow_zero_length_clientid true
|
||||||
|
|
||||||
|
log_timestamp_format %H:%M:%S
|
||||||
|
log_type all
|
||||||
|
|
||||||
|
# Plain
|
||||||
|
listener 1883
|
||||||
|
protocol mqtt
|
||||||
Reference in New Issue
Block a user