From f45f667af6d84e15b158dcfd927fad22b4fb5236 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 22 Oct 2021 08:01:18 -0400 Subject: [PATCH] Working on tests. --- Makefile | 11 ++++ Package.swift | 7 +++ Sources/Bootstrap/Bootstrap.swift | 4 +- Sources/Client/Interface.swift | 13 +++-- Sources/ClientLive/Helpers.swift | 78 ++++++++++++++++++-------- Sources/ClientLive/Live.swift | 12 ++-- Sources/Models/Topics.swift | 2 +- Sources/dewPoint-controller/main.swift | 6 +- Tests/ClientTests/ClientTests.swift | 78 ++++++++++++++++++++++++++ docker-compose.yaml | 22 ++++++++ mosquitto/config/mosquitto.conf | 10 ++++ 11 files changed, 208 insertions(+), 35 deletions(-) create mode 100644 Tests/ClientTests/ClientTests.swift create mode 100644 docker-compose.yaml create mode 100644 mosquitto/config/mosquitto.conf diff --git a/Makefile b/Makefile index 5be4655..8b586e4 100644 --- a/Makefile +++ b/Makefile @@ -10,3 +10,14 @@ build: run: @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 diff --git a/Package.swift b/Package.swift index 1a0c95a..3c18d97 100644 --- a/Package.swift +++ b/Package.swift @@ -80,5 +80,12 @@ let package = Package( .product(name: "MQTTNIO", package: "mqtt-nio") ] ), + .testTarget( + name: "ClientTests", + dependencies: [ + "Client", + "ClientLive" + ] + ), ] ) diff --git a/Sources/Bootstrap/Bootstrap.swift b/Sources/Bootstrap/Bootstrap.swift index b4ec3a1..930bb6c 100644 --- a/Sources/Bootstrap/Bootstrap.swift +++ b/Sources/Bootstrap/Bootstrap.swift @@ -120,7 +120,7 @@ extension EventLoopFuture where Value == (EnvVars, Topics) { map { envVars, topics in let nioClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger) return DewPointEnvironment.init( - mqttClient: .live(client: nioClient), + mqttClient: .live(client: nioClient, topics: topics), envVars: envVars, nioClient: nioClient, 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?) { self.init( diff --git a/Sources/Client/Interface.swift b/Sources/Client/Interface.swift index beeeafe..8d155ed 100644 --- a/Sources/Client/Interface.swift +++ b/Sources/Client/Interface.swift @@ -9,15 +9,18 @@ import Psychrometrics /// /// This is an abstraction around the ``MQTTNIO.MQTTClient``. public struct MQTTClient { - + /// Retrieve the humidity from the MQTT Broker. public var fetchHumidity: (Sensor) -> EventLoopFuture + /// Retrieve a set point from the MQTT Broker. + public var fetchSetPoint: (KeyPath) -> EventLoopFuture + /// Retrieve the temperature from the MQTT Broker. public var fetchTemperature: (Sensor, PsychrometricEnvironment.Units?) -> EventLoopFuture /// Publish a change of state message for a relay. - public var setRelay: (Relay, Relay.State) -> EventLoopFuture + public var setRelay: (KeyPath, Relay.State) -> EventLoopFuture /// Disconnect and close the connection to the MQTT Broker. public var shutdown: () -> EventLoopFuture @@ -27,12 +30,14 @@ public struct MQTTClient { public init( fetchHumidity: @escaping (Sensor) -> EventLoopFuture, + fetchSetPoint: @escaping (KeyPath) -> EventLoopFuture, fetchTemperature: @escaping (Sensor, PsychrometricEnvironment.Units?) -> EventLoopFuture, - setRelay: @escaping (Relay, Relay.State) -> EventLoopFuture, + setRelay: @escaping (KeyPath, Relay.State) -> EventLoopFuture, shutdown: @escaping () -> EventLoopFuture, publishDewPoint: @escaping (DewPoint, String) -> EventLoopFuture ) { self.fetchHumidity = fetchHumidity + self.fetchSetPoint = fetchSetPoint self.fetchTemperature = fetchTemperature self.setRelay = setRelay self.shutdown = shutdown @@ -60,7 +65,7 @@ public struct MQTTClient { /// - Parameters: /// - relay: The relay to send the message to. /// - state: The state to change the relay to. - public func `set`(relay: Relay, to state: Relay.State) -> EventLoopFuture { + public func `set`(relay: KeyPath, to state: Relay.State) -> EventLoopFuture { setRelay(relay, state) } diff --git a/Sources/ClientLive/Helpers.swift b/Sources/ClientLive/Helpers.swift index f3dcba7..f264aa8 100644 --- a/Sources/ClientLive/Helpers.swift +++ b/Sources/ClientLive/Helpers.swift @@ -43,8 +43,55 @@ enum MQTTError: 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 { + 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( + _ subscription: MQTTSubscribeInfoV5 + ) -> EventLoopFuture 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``. /// /// - Parameters: @@ -52,32 +99,17 @@ extension MQTTNIO.MQTTClient { func fetch( sensor: Sensor ) -> EventLoopFuture where S: BufferInitalizable { - logger.debug("Fetching data for sensor: \(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 - } + return fetch(mqttSubscription(topic: sensor.topic)) } - func `set`(relay: Relay, to state: Relay.State, qos: MQTTQoS = .atLeastOnce) -> EventLoopFuture { + func fetch(setPoint: KeyPath, setPoints: Topics.SetPoints) -> EventLoopFuture { +// 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 { publish( - to: relay.topic, + to: relayTopic, payload: ByteBufferAllocator().buffer(string: state.rawValue), qos: qos ) diff --git a/Sources/ClientLive/Live.swift b/Sources/ClientLive/Live.swift index e5abec6..e228ebc 100644 --- a/Sources/ClientLive/Live.swift +++ b/Sources/ClientLive/Live.swift @@ -1,5 +1,5 @@ import Foundation -import Client +@_exported import Client import CoreUnitTypes import Models import MQTTNIO @@ -11,20 +11,24 @@ extension Client.MQTTClient { /// /// - Parameters: /// - 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( fetchHumidity: { sensor in client.fetch(sensor: sensor) .debug(logger: client.logger) }, + fetchSetPoint: { setPointKeyPath in + client.fetch(client.mqttSubscription(topic: topics.setPoints[keyPath: setPointKeyPath])) + .debug(logger: client.logger) + }, fetchTemperature: { sensor, units in client.fetch(sensor: sensor) .debug(logger: client.logger) .convertIfNeeded(to: units) .debug(logger: client.logger) }, - setRelay: { relay, state in - client.set(relay: relay, to: state) + setRelay: { relayKeyPath, state in + client.set(relay: topics.commands.relays[keyPath: relayKeyPath], to: state) }, shutdown: { client.disconnect() diff --git a/Sources/Models/Topics.swift b/Sources/Models/Topics.swift index 88c8666..93ce1b0 100644 --- a/Sources/Models/Topics.swift +++ b/Sources/Models/Topics.swift @@ -5,7 +5,7 @@ public struct Topics: Codable, Equatable { /// The command topics the application can publish to. 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 /// The set point topics the application can read set point values from. diff --git a/Sources/dewPoint-controller/main.swift b/Sources/dewPoint-controller/main.swift index 39840bb..7001ee1 100644 --- a/Sources/dewPoint-controller/main.swift +++ b/Sources/dewPoint-controller/main.swift @@ -34,7 +34,11 @@ defer { while true { // let temp = try environment.mqttClient.fetchTemperature(tempSensor, .imperial).wait() // 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...") let dp = try environment.mqttClient.currentDewPoint( diff --git a/Tests/ClientTests/ClientTests.swift b/Tests/ClientTests/ClientTests.swift new file mode 100644 index 0000000..97c6c31 --- /dev/null +++ b/Tests/ClientTests/ClientTests.swift @@ -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 + }() +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..5f06f16 --- /dev/null +++ b/docker-compose.yaml @@ -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" diff --git a/mosquitto/config/mosquitto.conf b/mosquitto/config/mosquitto.conf new file mode 100644 index 0000000..cd3b3e3 --- /dev/null +++ b/mosquitto/config/mosquitto.conf @@ -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