diff --git a/Makefile b/Makefile index d6dcfaf..53d3480 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,8 @@ bootstrap-env: bootstrap-topics: @cp Bootstrap/topics-example .topics + +bootstrap: bootstrap-env bootstrap-topics build: @swift build diff --git a/Package.swift b/Package.swift index dbba5dd..7186b7b 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,8 @@ let package = Package( name: "dewPoint-controller", dependencies: [ "Bootstrap", + "ClientLive", + "TopicsLive", .product(name: "MQTTNIO", package: "mqtt-nio"), .product(name: "NIO", package: "swift-nio") ] @@ -87,5 +89,11 @@ let package = Package( "ClientLive" ] ), + .target( + name: "TopicsLive", + dependencies: [ + "Models" + ] + ), ] ) diff --git a/Sources/ClientLive/Helpers.swift b/Sources/ClientLive/Helpers.swift index 660315e..b46f86a 100644 --- a/Sources/ClientLive/Helpers.swift +++ b/Sources/ClientLive/Helpers.swift @@ -10,32 +10,34 @@ protocol BufferInitalizable { init?(buffer: inout ByteBuffer) } -extension Temperature: BufferInitalizable { +extension Double: BufferInitalizable { + /// Attempt to create / parse a double from a byte buffer. init?(buffer: inout ByteBuffer) { - guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8), - let value = Double(string) - else { - return nil - } + guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8) + else { return nil } + self.init(string) + } +} + +extension Temperature: BufferInitalizable { + /// Attempt to create / parse a temperature from a byte buffer. + init?(buffer: inout ByteBuffer) { + guard let value = Double(buffer: &buffer) else { return nil } self.init(value, units: .celsius) } } extension RelativeHumidity: BufferInitalizable { - + /// Attempt to create / parse a relative humidity from a byte buffer. init?(buffer: inout ByteBuffer) { - guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8), - let value = Double(string) - else { - return nil - } + guard let value = Double(buffer: &buffer) else { return nil } self.init(value) } } extension MQTTNIO.MQTTClient { - + /// Logs a failure for a given topic and error. func logFailure(topic: String, error: Error) { logger.error("\(topic): \(error)") } @@ -214,10 +216,11 @@ extension MQTTNIO.MQTTClient { logger.trace("No dew point for sensor.") return eventLoopGroup.next().makeSucceededFuture((self, request, state, topics)) } + let roundedDewPoint = round(dewPoint.rawValue * 100) / 100 logger.debug("Publishing dew-point: \(dewPoint), to: \(topic)") return publish( to: topic, - payload: ByteBufferAllocator().buffer(string: "\(dewPoint.rawValue)"), + payload: ByteBufferAllocator().buffer(string: "\(roundedDewPoint)"), qos: .atLeastOnce ) .map { (self, request, state, topics) } @@ -240,10 +243,11 @@ extension EventLoopFuture where Value == (MQTTNIO.MQTTClient, Client.SensorPubli client.logger.trace("No enthalpy for sensor.") return client.eventLoopGroup.next().makeSucceededFuture((request, state)) } + let roundedEnthalpy = round(enthalpy.rawValue * 100) / 100 client.logger.debug("Publishing enthalpy: \(enthalpy), to: \(topic)") return client.publish( to: topic, - payload: ByteBufferAllocator().buffer(string: "\(enthalpy.rawValue)"), + payload: ByteBufferAllocator().buffer(string: "\(roundedEnthalpy)"), qos: .atLeastOnce ) .map { (request, state) } diff --git a/Sources/Models/State.swift b/Sources/Models/State.swift index f54d8b6..06fe870 100644 --- a/Sources/Models/State.swift +++ b/Sources/Models/State.swift @@ -70,14 +70,18 @@ extension State.Sensors { public func dewPoint(units: PsychrometricEnvironment.Units? = nil) -> DewPoint? { guard let temperature = temperature, - let humidity = humidity + let humidity = humidity, + !temperature.rawValue.isNaN, + !humidity.rawValue.isNaN else { return nil } return .init(dryBulb: temperature, humidity: humidity, units: units) } public func enthalpy(altitude: Length, units: PsychrometricEnvironment.Units? = nil) -> EnthalpyOf? { guard let temperature = temperature, - let humidity = humidity + let humidity = humidity, + !temperature.rawValue.isNaN, + !humidity.rawValue.isNaN else { return nil } return .init(dryBulb: temperature, humidity: humidity, altitude: altitude, units: units) } diff --git a/Sources/TopicsLive/Live.swift b/Sources/TopicsLive/Live.swift new file mode 100644 index 0000000..3a4ad57 --- /dev/null +++ b/Sources/TopicsLive/Live.swift @@ -0,0 +1,52 @@ +import Models + +// TODO: Fix other live topics +extension Topics { + + public static let live = Self.init( + commands: .init(), + sensors: .init( + mixedAirSensor: .live(location: .mixedAir), + postCoilSensor: .live(location: .postCoil), + returnAirSensor: .live(location: .return), + supplyAirSensor: .live(location: .supply)), + setPoints: .init(), + states: .init() + ) +} + +extension Topics.Sensors { + fileprivate enum Location: CustomStringConvertible { + case mixedAir + case postCoil + case `return` + case supply + + var description: String { + switch self { + case .mixedAir: + return "mixed_air" + case .postCoil: + return "post_coil" + case .return: + return "return" + case .supply: + return "supply" + } + } + } +} + +extension Topics.Sensors.TemperatureAndHumiditySensor { + fileprivate static func live( + prefix: String = "frankensystem", + location: Topics.Sensors.Location + ) -> Self { + .init( + temperature: "\(prefix)/sensor/\(location.description)_temperature/state", + humidity: "\(prefix)/sensor/\(location.description)_humidity/state", + dewPoint: "\(prefix)/sensor/\(location.description)_dew_point/state", + enthalpy: "\(prefix)/sensor/\(location.description)_enthalpy/state" + ) + } +} diff --git a/Sources/dewPoint-controller/main.swift b/Sources/dewPoint-controller/main.swift index 6e335e1..06a5e4d 100644 --- a/Sources/dewPoint-controller/main.swift +++ b/Sources/dewPoint-controller/main.swift @@ -1,60 +1,73 @@ import Bootstrap +import ClientLive import CoreUnitTypes import Logging import Models import MQTTNIO import NIO +import TopicsLive import Foundation var logger: Logger = { var logger = Logger(label: "dewPoint-logger") - logger.logLevel = .info + logger.logLevel = .debug return logger }() logger.info("Starting Swift Dew Point Controller!") let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) -let environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger).wait() +var environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger, autoConnect: false).wait() // Set the log level to info only in production mode. if environment.envVars.appEnv == .production { logger.logLevel = .info } -//let relay = Relay(topic: environment.topics.commands.relays.dehumidification1) -//let tempSensor = Sensor(topic: environment.topics.sensors.returnAirSensor.temperature) -//let humiditySensor = Sensor(topic: environment.topics.sensors.returnAirSensor.humidity) +// Set up the client, topics and state. +environment.topics = .live +let state = State() +let client = Client.live(client: environment.mqttClient, state: state, topics: environment.topics) defer { logger.debug("Disconnecting") -// try? environment.mqttClient.shutdown().wait() } +// Add topic listeners. +client.addListeners() + while true { -// let temp = try environment.mqttClient.fetchTemperature(tempSensor, .imperial).wait() -// logger.debug("Temp: \(temp.rawValue)") + if !environment.mqttClient.isActive() { + logger.trace("Connecting to MQTT broker...") + try client.connect().wait() + try client.subscribe().wait() + Thread.sleep(forTimeInterval: 1) + } + + // Check if sensors need processed. + if state.sensors.needsProcessed { + logger.debug("Sensor state has changed...") + if state.sensors.mixedAirSensor.needsProcessed { + logger.trace("Publishing mixed air sensor.") + try client.publishSensor(.mixed(state.sensors.mixedAirSensor)).wait() + } + if state.sensors.postCoilSensor.needsProcessed { + logger.trace("Publishing post coil sensor.") + try client.publishSensor(.postCoil(state.sensors.postCoilSensor)).wait() + } + if state.sensors.returnAirSensor.needsProcessed { + logger.trace("Publishing return air sensor.") + try client.publishSensor(.return(state.sensors.returnAirSensor)).wait() + } + if state.sensors.supplyAirSensor.needsProcessed { + logger.trace("Publishing supply air sensor.") + try client.publishSensor(.supply(state.sensors.supplyAirSensor)).wait() + } + } + +// logger.debug("Fetching dew point...") // -// 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( -// temperature: tempSensor, -// humidity: humiditySensor, -// units: .imperial -// ).wait() - -// logger.info("Dew Point: \(dp.rawValue) \(dp.units.symbol)") - -// try environment.mqttClient.publish( -// dewPoint: dp, -// to: environment.topics.sensors.returnAirSensor.dewPoint -// ).wait() - - logger.debug("Published dew point...") +// logger.debug("Published dew point...") Thread.sleep(forTimeInterval: 5) } diff --git a/Tests/ClientTests/ClientTests.swift b/Tests/ClientTests/ClientTests.swift index 71a16f1..3d13c61 100644 --- a/Tests/ClientTests/ClientTests.swift +++ b/Tests/ClientTests/ClientTests.swift @@ -9,7 +9,6 @@ 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()