From 3f78a53014e03e54ae2368234cc7d6f76a32ca58 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Sun, 17 Oct 2021 15:52:15 -0400 Subject: [PATCH] Cleaning up the api. --- Bootstrap/dewPoint-env-example | 8 +- Package.swift | 6 +- Sources/Bootstrap/Bootstrap.swift | 90 ++++++++++-- Sources/Client/Interface.swift | 86 ++++++++--- Sources/ClientLive/Helpers.swift | 139 ++++++++++++++++++ Sources/ClientLive/Live.swift | 138 +++-------------- Sources/DewPointEnvironment/Environment.swift | 14 +- Sources/EnvVars/EnvVars.swift | 29 +++- Sources/Models/HumiditySensor.swift | 8 - Sources/Models/Mode.swift | 37 +++++ Sources/Models/Relay.swift | 32 ++++ Sources/Models/Sensor.swift | 15 ++ Sources/Models/TemperatureSensor.swift | 8 - Sources/Models/Topics.swift | 91 ++++++++++++ Sources/dewPoint-controller/main.swift | 52 ++++--- 15 files changed, 551 insertions(+), 202 deletions(-) create mode 100644 Sources/ClientLive/Helpers.swift delete mode 100644 Sources/Models/HumiditySensor.swift create mode 100644 Sources/Models/Mode.swift create mode 100644 Sources/Models/Sensor.swift delete mode 100644 Sources/Models/TemperatureSensor.swift create mode 100644 Sources/Models/Topics.swift diff --git a/Bootstrap/dewPoint-env-example b/Bootstrap/dewPoint-env-example index 48be2bc..6643493 100644 --- a/Bootstrap/dewPoint-env-example +++ b/Bootstrap/dewPoint-env-example @@ -4,5 +4,11 @@ "MQTT_PORT": "1883", "MQTT_IDENTIFIER": "dewPoint-controller", "MQTT_USERNAME": "mqtt_user", - "MQTT_PASSWORD": "secret!" + "MQTT_PASSWORD": "secret!", + "DEHUMIDIFICATION_STAGE_1_RELAY": "relays/dehumidification_1", + "DEHUMIDIFICATION_STAGE_2_RELAY": "relays/dehumidification_2", + "DEW_POINT_TOPIC": "sensors/dew_point", + "HUMIDIFICATION_RELAY": "relays/humidification", + "HUMIDITY_SENSOR": "sensors/humidity", + "TEMPERATURE_SENSOR": "sensors/temperature" } diff --git a/Package.swift b/Package.swift index 0869a86..1a0c95a 100644 --- a/Package.swift +++ b/Package.swift @@ -40,6 +40,7 @@ let package = Package( "DewPointEnvironment", "EnvVars", "ClientLive", + "Models", .product(name: "MQTTNIO", package: "mqtt-nio"), .product(name: "NIO", package: "swift-nio") ] @@ -49,6 +50,7 @@ let package = Package( dependencies: [ "EnvVars", "Client", + "Models", .product(name: "MQTTNIO", package: "mqtt-nio"), ] ), @@ -58,7 +60,9 @@ let package = Package( ), .target( name: "Models", - dependencies: [] + dependencies: [ + .product(name: "CoreUnitTypes", package: "swift-psychrometrics"), + ] ), .target( name: "Client", diff --git a/Sources/Bootstrap/Bootstrap.swift b/Sources/Bootstrap/Bootstrap.swift index 08026c2..a33e285 100644 --- a/Sources/Bootstrap/Bootstrap.swift +++ b/Sources/Bootstrap/Bootstrap.swift @@ -3,30 +3,32 @@ import DewPointEnvironment import EnvVars import Logging import Foundation +import Models import MQTTNIO import NIO +/// Sets up the application environment and connections required. +/// +/// - Parameters: +/// - eventLoopGroup: The event loop group for the application. +/// - logger: An optional logger for debugging. public func bootstrap( eventLoopGroup: EventLoopGroup, logger: Logger? = nil ) -> EventLoopFuture { + logger?.debug("Bootstrapping Dew Point Controller...") + return loadEnvVars(eventLoopGroup: eventLoopGroup, logger: logger) - .map { (envVars) -> DewPointEnvironment in - let mqttClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger) - return DewPointEnvironment.init( - client: .live(client: mqttClient), - envVars: envVars, - mqttClient: mqttClient - ) - } - .flatMap { environment in - environment.mqttClient.logger.debug("Connecting to MQTT broker...") - return environment.mqttClient.connect() - .map { _ in environment } - } + .makeDewPointEnvironment(eventLoopGroup: eventLoopGroup, logger: logger) + .connectToMQTTBroker(logger: logger) } +/// Loads the ``EnvVars`` either using the defualts, from a file in the root directory under `.dewPoint-env` or in the shell / application environment. +/// +/// - Parameters: +/// - eventLoopGroup: The event loop group for the application. +/// - logger: An optional logger for debugging. private func loadEnvVars(eventLoopGroup: EventLoopGroup, logger: Logger?) -> EventLoopFuture { logger?.debug("Loading env vars...") @@ -66,6 +68,49 @@ private func loadEnvVars(eventLoopGroup: EventLoopGroup, logger: Logger?) -> Eve return eventLoopGroup.next().makeSucceededFuture(envVars) } +extension EventLoopFuture where Value == EnvVars { + + /// Creates the ``DewPointEnvironment`` for the application after the ``EnvVars`` have been loaded. + /// + /// - Parameters: + /// - eventLoopGroup: The event loop group for the application. + /// - logger: An optional logger for the application. + fileprivate func makeDewPointEnvironment( + eventLoopGroup: EventLoopGroup, + logger: Logger? + ) -> EventLoopFuture { + map { envVars in + let nioClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger) + return DewPointEnvironment.init( + mqttClient: .live(client: nioClient), + envVars: envVars, + nioClient: nioClient, + topics: .init(envVars: envVars) + ) + } + } +} + +extension EventLoopFuture where Value == DewPointEnvironment { + + /// Connects to the MQTT broker after the ``DewPointEnvironment`` has been setup. + /// + /// - Parameters: + /// - logger: An optional logger for debugging. + fileprivate func connectToMQTTBroker(logger: Logger?) -> EventLoopFuture { + flatMap { environment in + logger?.debug("Connecting to MQTT Broker...") + return environment.nioClient.connect() + .map { _ in + logger?.debug("Successfully connected to MQTT Broker...") + return environment + } + } + } +} + +// MARK: - Helpers + extension MQTTClient { convenience init(envVars: EnvVars, eventLoopGroup: EventLoopGroup, logger: Logger?) { @@ -83,3 +128,22 @@ extension MQTTClient { ) } } + +// MARK: - TODO Make topics loadable from a file in the root directory. +extension Topics { + + init(envVars: EnvVars) { + self.init( + sensors: .init( + temperature: envVars.temperatureSensor, + humidity: envVars.humiditySensor, + dewPoint: envVars.dewPointTopic + ), + relays: .init( + dehumidification1: envVars.dehumidificationStage1Relay, + dehumidification2: envVars.dehumidificationStage2Relay, + humidification: envVars.humidificationRelay + ) + ) + } +} diff --git a/Sources/Client/Interface.swift b/Sources/Client/Interface.swift index 7d0aade..beeeafe 100644 --- a/Sources/Client/Interface.swift +++ b/Sources/Client/Interface.swift @@ -5,42 +5,80 @@ import Models import NIO import Psychrometrics -public struct Client { +/// Represents the applications interactions with the MQTT Broker. +/// +/// This is an abstraction around the ``MQTTNIO.MQTTClient``. +public struct MQTTClient { - public var fetchHumidity: (HumiditySensor) -> EventLoopFuture - public var fetchTemperature: (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture - public var toggleRelay: (Relay) -> EventLoopFuture - public var turnOnRelay: (Relay) -> EventLoopFuture - public var turnOffRelay: (Relay) -> EventLoopFuture + /// Retrieve the humidity from the MQTT Broker. + public var fetchHumidity: (Sensor) -> 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 + + /// Disconnect and close the connection to the MQTT Broker. public var shutdown: () -> EventLoopFuture + /// Publish the current dew point to the MQTT Broker + public var publishDewPoint: (DewPoint, String) -> EventLoopFuture + public init( - fetchHumidity: @escaping (HumiditySensor) -> EventLoopFuture, - fetchTemperature: @escaping (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture, - toggleRelay: @escaping (Relay) -> EventLoopFuture, - turnOnRelay: @escaping (Relay) -> EventLoopFuture, - turnOffRelay: @escaping (Relay) -> EventLoopFuture, - shutdown: @escaping () -> EventLoopFuture + fetchHumidity: @escaping (Sensor) -> EventLoopFuture, + fetchTemperature: @escaping (Sensor, PsychrometricEnvironment.Units?) -> EventLoopFuture, + setRelay: @escaping (Relay, Relay.State) -> EventLoopFuture, + shutdown: @escaping () -> EventLoopFuture, + publishDewPoint: @escaping (DewPoint, String) -> EventLoopFuture ) { self.fetchHumidity = fetchHumidity self.fetchTemperature = fetchTemperature - self.toggleRelay = toggleRelay - self.turnOnRelay = turnOnRelay - self.turnOffRelay = turnOffRelay + self.setRelay = setRelay self.shutdown = shutdown + self.publishDewPoint = publishDewPoint } - public func fetchDewPoint( - temperature: TemperatureSensor, - humidity: HumiditySensor, - units: PsychrometricEnvironment.Units? = nil, - logger: Logger? = nil + /// Fetches the current temperature and humidity and calculates the current dew point. + /// + /// - Parameters: + /// - temperature: The temperature sensor to fetch the temperature from. + /// - humidity: The humidity sensor to fetch the humidity from. + /// - units: Optional units for the dew point. + public func currentDewPoint( + temperature: Sensor, + humidity: Sensor, + units: PsychrometricEnvironment.Units? = nil ) -> EventLoopFuture { fetchTemperature(temperature, units) .and(fetchHumidity(humidity)) - .map { temp, humidity in - logger?.debug("Creating dew-point for temperature: \(temp) with humidity: \(humidity)") - return DewPoint.init(dryBulb: temp, humidity: humidity, units: units) - } + .convertToDewPoint(units: units) + } + + /// Convenience to send a change of state message to a relay. + /// + /// - 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 { + setRelay(relay, state) + } + + /// Convenience to publish the current dew point back to the MQTT Broker. + /// + /// This is synactic sugar around ``MQTTClient.publishDewPoint``. + /// + /// - Parameters: + /// - dewPoint: The dew point value to publish. + /// - topic: The dew point topic to publish to. + public func publish(dewPoint: DewPoint, to topic: String) -> EventLoopFuture { + publishDewPoint(dewPoint, topic) + } +} + +extension EventLoopFuture where Value == (Temperature, RelativeHumidity) { + + fileprivate func convertToDewPoint(units: PsychrometricEnvironment.Units?) -> EventLoopFuture { + map { .init(dryBulb: $0, humidity: $1, units: units) } } } diff --git a/Sources/ClientLive/Helpers.swift b/Sources/ClientLive/Helpers.swift new file mode 100644 index 0000000..56dc09d --- /dev/null +++ b/Sources/ClientLive/Helpers.swift @@ -0,0 +1,139 @@ +import CoreUnitTypes +import Models +import MQTTNIO +import NIO + +/// Represents a type that can be initialized by a ``ByteBuffer``. +protocol BufferInitalizable { + init?(buffer: inout ByteBuffer) +} + +extension Temperature: BufferInitalizable { + + init?(buffer: inout ByteBuffer) { + guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8), + let value = Double(string) + else { + return nil + } + self.init(value, units: .celsius) + } +} + +extension RelativeHumidity: BufferInitalizable { + + init?(buffer: inout ByteBuffer) { + guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8), + let value = Double(string) + else { + return nil + } + self.init(value) + } +} + +/// Represents errors thrown while communicating with the MQTT Broker. +enum MQTTError: Error { + + /// Sensor error. + case sensor(reason: String, error: Error?) + + /// Relay error. + case relay(reason: String, error: Error?) +} + +extension MQTTNIO.MQTTClient { + + /// Fetch a sensor state and convert it appropriately, when the sensor type is ``BufferInitializable``. + /// + /// - Parameters: + /// - sensor: The sensor to fetch the state of. + 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: true, + 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 { + publish( + to: relay.topic, + payload: ByteBufferAllocator().buffer(string: state.rawValue), + qos: qos + ) + } +} + +extension Result where Success == MQTTPublishInfo, Failure == Error { + + func mapBuffer(to type: S.Type) -> Result where S: BufferInitalizable { + map { info in + var buffer = info.payload + return S.init(buffer: &buffer) + } + } +} + +extension Result { + + func fullfill(promise: EventLoopPromise) { + switch self { + case let.success(value): + promise.succeed(value) + case let .failure(error): + promise.fail(error) + } + } + +} + +extension Result where Failure == Error { + + func unwrap( + or error: @autoclosure @escaping () -> F + ) -> Result where Success == Optional, Failure == F { + flatMap { optionalResult in + guard let value = optionalResult else { + return .failure(error()) + } + return .success(value) + } + } +} + +extension Temperature { + + func convert(to units: PsychrometricEnvironment.Units) -> Self { + let temperatureUnits = Units.defaultFor(units: units) + return .init(self[temperatureUnits], units: temperatureUnits) + } +} + +extension EventLoopFuture where Value == Temperature { + + func convertIfNeeded(to units: PsychrometricEnvironment.Units?) -> EventLoopFuture { + map { currentTemperature in + guard let units = units else { return currentTemperature } + return currentTemperature.convert(to: units) + } + } +} diff --git a/Sources/ClientLive/Live.swift b/Sources/ClientLive/Live.swift index af809ca..1e58178 100644 --- a/Sources/ClientLive/Live.swift +++ b/Sources/ClientLive/Live.swift @@ -5,135 +5,35 @@ import Models import MQTTNIO import NIO -extension Client { +extension Client.MQTTClient { - public static func live(client: MQTTClient) -> Self { + /// Creates the live implementation of our ``Client.MQTTClient`` for the application. + /// + /// - Parameters: + /// - client: The ``MQTTNIO.MQTTClient`` used to send and recieve messages from the MQTT Broker. + public static func live(client: MQTTNIO.MQTTClient) -> Self { .init( fetchHumidity: { sensor in - client.fetchHumidity(sensor: sensor) + client.fetch(sensor: sensor) }, fetchTemperature: { sensor, units in - client.fetchTemperature(sensor: sensor, units: units) + client.fetch(sensor: sensor) + .convertIfNeeded(to: units) }, - toggleRelay: { relay in - client.publish(relay: relay, state: .toggle, qos: .atLeastOnce) - }, - turnOnRelay: { relay in - client.publish(relay: relay, state: .on, qos: .atLeastOnce) - }, - turnOffRelay: { relay in - client.publish(relay: relay, state: .off, qos: .atLeastOnce) + setRelay: { relay, state in + client.set(relay: relay, to: state) }, shutdown: { client.disconnect() + .map { try? client.syncShutdownGracefully() } + }, + publishDewPoint: { dewPoint, topic in + client.publish( + to: topic, + payload: ByteBufferAllocator().buffer(string: "\(dewPoint.rawValue)"), + qos: .atLeastOnce + ) } ) } } - -// MARK: - Helpers -enum TemperatureError: Error { - case invalidTemperature -} - -enum HumidityError: Error { - case invalidHumidity -} - -extension Relay { - enum State: String { - case toggle, on, off - } -} - -extension MQTTClient { - - fileprivate func publish(relay: Relay, state: Relay.State, qos: MQTTQoS = .atLeastOnce) -> EventLoopFuture { - publish( - to: relay.topic, - payload: ByteBufferAllocator().buffer(string: state.rawValue), - qos: qos - ) - } - - // MARK: - TODO it feels like the subscriptions should happen in the `bootstrap` process. - fileprivate func fetchTemperature( - sensor: TemperatureSensor, - units: PsychrometricEnvironment.Units? - ) -> EventLoopFuture { - logger.debug("Adding listener for temperature sensor...") - let subscription = MQTTSubscribeInfoV5.init( - topicFilter: sensor.topic, - qos: .atLeastOnce, - retainAsPublished: true, - retainHandling: .sendAlways - ) - return v5.subscribe(to: [subscription]) - .flatMap { _ in - let promise = self.eventLoopGroup.next().makePromise(of: Temperature.self) - self.addPublishListener(named: "temperature-sensor", { result in - switch result.temperature() { - case let .success(celsius): - let userUnits = units ?? PsychrometricEnvironment.shared.units - let temperatureUnits = Temperature.Units.defaultFor(units: userUnits) - promise.succeed(.init(celsius[temperatureUnits], units: temperatureUnits)) - case let .failure(error): - promise.fail(error) - } - }) - - return promise.futureResult - } - } - - // MARK: - TODO it feels like the subscriptions should happen in the `bootstrap` process. - fileprivate func fetchHumidity(sensor: HumiditySensor) -> EventLoopFuture { - logger.debug("Adding listener for humidity sensor...") - let subscription = MQTTSubscribeInfoV5.init( - topicFilter: sensor.topic, - qos: .atLeastOnce, - retainAsPublished: true, - retainHandling: .sendAlways - ) - return v5.subscribe(to: [subscription]) - .flatMap { _ in - let promise = self.eventLoopGroup.next().makePromise(of: RelativeHumidity.self) - self.addPublishListener(named: "humidity-sensor", { result in - switch result.humidity() { - case let .success(humidity): - promise.succeed(humidity) - case let .failure(error): - promise.fail(error) - } - }) - return promise.futureResult - } - } -} - -extension Result where Success == MQTTPublishInfo, Failure == Error { - - fileprivate func humidity() -> Result { - flatMap { info in - var buffer = info.payload - guard let string = buffer.readString(length: buffer.readableBytes), - let double = Double(string) - else { - return .failure(HumidityError.invalidHumidity) - } - return .success(.init(double)) - } - } - - fileprivate func temperature() -> Result { - flatMap { info in - var buffer = info.payload - guard let string = buffer.readString(length: buffer.readableBytes), - let temperatureValue = Double(string) - else { - return .failure(TemperatureError.invalidTemperature) - } - return .success(.celsius(temperatureValue)) - } - } -} diff --git a/Sources/DewPointEnvironment/Environment.swift b/Sources/DewPointEnvironment/Environment.swift index 4c8f11e..fd804a3 100644 --- a/Sources/DewPointEnvironment/Environment.swift +++ b/Sources/DewPointEnvironment/Environment.swift @@ -1,20 +1,24 @@ import Client import EnvVars +import Models import MQTTNIO public struct DewPointEnvironment { - public var client: Client + public var mqttClient: Client.MQTTClient public var envVars: EnvVars - public var mqttClient: MQTTClient + public var nioClient: MQTTNIO.MQTTClient + public var topics: Topics public init( - client: Client, + mqttClient: Client.MQTTClient, envVars: EnvVars, - mqttClient: MQTTClient + nioClient: MQTTNIO.MQTTClient, + topics: Topics = .init() ) { self.mqttClient = mqttClient self.envVars = envVars - self.client = client + self.nioClient = nioClient + self.topics = topics } } diff --git a/Sources/EnvVars/EnvVars.swift b/Sources/EnvVars/EnvVars.swift index 47f4a26..f8e558f 100644 --- a/Sources/EnvVars/EnvVars.swift +++ b/Sources/EnvVars/EnvVars.swift @@ -24,6 +24,15 @@ public struct EnvVars: Codable, Equatable { /// The MQTT user password. public var password: String? + // MARK: TODO Move Topics to their own file that can be loaded. + // Topics + public var dehumidificationStage1Relay: String + public var dehumidificationStage2Relay: String + public var dewPointTopic: String + public var humidificationRelay: String + public var humiditySensor: String + public var temperatureSensor: String + /// Create a new ``EnvVars`` /// /// - Parameters: @@ -39,7 +48,13 @@ public struct EnvVars: Codable, Equatable { port: String? = "1883", identifier: String = "dewPoint-controller", userName: String? = "mqtt_user", - password: String? = "secret!" + password: String? = "secret!", + dehumidificationStage1Relay: String = "relays/dehumidification_1", + dehumidificationStage2Relay: String = "relays/dehumidification_2", + dewPointTopic: String = "sensors/dew_point", + humidificationRelay: String = "relays/humidification", + humiditySensor: String = "sensors/humidity", + temperatureSensor: String = "sensors/temperature" ){ self.appEnv = appEnv self.host = host @@ -47,6 +62,12 @@ public struct EnvVars: Codable, Equatable { self.identifier = identifier self.userName = userName self.password = password + self.dehumidificationStage1Relay = dehumidificationStage1Relay + self.dehumidificationStage2Relay = dehumidificationStage2Relay + self.dewPointTopic = dewPointTopic + self.humidificationRelay = humidificationRelay + self.humiditySensor = humiditySensor + self.temperatureSensor = temperatureSensor } /// Custom coding keys. @@ -57,6 +78,12 @@ public struct EnvVars: Codable, Equatable { case identifier = "MQTT_IDENTIFIER" case userName = "MQTT_USERNAME" case password = "MQTT_PASSWORD" + case dehumidificationStage1Relay = "DEHUMIDIFICATION_STAGE_1_RELAY" + case dehumidificationStage2Relay = "DEHUMIDIFICATION_STAGE_2_RELAY" + case dewPointTopic = "DEW_POINT_TOPIC" + case humidificationRelay = "HUMIDIFICATION_RELAY" + case humiditySensor = "HUMIDITY_SENSOR" + case temperatureSensor = "TEMPERATURE_SENSOR" } /// Represents the different app environments. diff --git a/Sources/Models/HumiditySensor.swift b/Sources/Models/HumiditySensor.swift deleted file mode 100644 index 5cd04ca..0000000 --- a/Sources/Models/HumiditySensor.swift +++ /dev/null @@ -1,8 +0,0 @@ - -public struct HumiditySensor: Equatable { - public var topic: String - - public init(topic: String) { - self.topic = topic - } -} diff --git a/Sources/Models/Mode.swift b/Sources/Models/Mode.swift new file mode 100644 index 0000000..de7ef8d --- /dev/null +++ b/Sources/Models/Mode.swift @@ -0,0 +1,37 @@ +import CoreUnitTypes + +/// Represents the different modes that the controller can be in. +public enum Mode: Equatable { + + /// Allows controller to run in humidify or dehumidify mode. + case auto + + /// Only handle humidify mode. + case humidifyOnly(HumidifyMode) + + /// Only handle dehumidify mode. + case dehumidifyOnly(DehumidifyMode) + + /// Don't control humidify or dehumidify modes. + case off + + /// Represents the control modes for the humidify control state. + public enum HumidifyMode: Equatable { + + /// Control humidifying based off dew-point. + case dewPoint(Temperature) + + /// Control humidifying based off relative humidity. + case relativeHumidity(RelativeHumidity) + } + + /// Represents the control modes for the dehumidify control state. + public enum DehumidifyMode: Equatable { + + /// Control dehumidifying based off dew-point. + case dewPoint(high: Temperature, low: Temperature) + + /// Control humidifying based off relative humidity. + case relativeHumidity(high: RelativeHumidity, low: RelativeHumidity) + } +} diff --git a/Sources/Models/Relay.swift b/Sources/Models/Relay.swift index 728d6d7..c337a08 100644 --- a/Sources/Models/Relay.swift +++ b/Sources/Models/Relay.swift @@ -1,8 +1,40 @@ +/// Represents a relay that can be controlled by the MQTT Broker. public struct Relay { + + /// The topic for the relay. public var topic: String + /// Create a new relay at the given topic. + /// + /// - Parameters: + /// - topic: The topic for commanding the relay. public init(topic: String) { self.topic = topic } } + +public enum Relay2 { + + /// The topic to read the current state of the relay from. + case read(topic: String) + + /// The topic to command the relay state. + case command(topic: String) +} + +extension Relay { + + /// Represents the different commands that can be sent to a relay. + public enum State: String { + + /// Toggle the relay state on or off based on it's current state. + case toggle + + /// Turn the relay off. + case off + + /// Turn the relay on. + case on + } +} diff --git a/Sources/Models/Sensor.swift b/Sources/Models/Sensor.swift new file mode 100644 index 0000000..e6341bb --- /dev/null +++ b/Sources/Models/Sensor.swift @@ -0,0 +1,15 @@ + +/// Represents a sensor that provides a reading. +public struct Sensor: Equatable { + + /// The topic to retrieve the reading from. + public var topic: String + + /// Create a new sensor for the given topic. + /// + /// - Parameters: + /// - topic: The topic to retrieve the readings from. + public init(topic: String) { + self.topic = topic + } +} diff --git a/Sources/Models/TemperatureSensor.swift b/Sources/Models/TemperatureSensor.swift deleted file mode 100644 index 63a6493..0000000 --- a/Sources/Models/TemperatureSensor.swift +++ /dev/null @@ -1,8 +0,0 @@ - -public struct TemperatureSensor: Equatable { - public var topic: String - - public init(topic: String) { - self.topic = topic - } -} diff --git a/Sources/Models/Topics.swift b/Sources/Models/Topics.swift new file mode 100644 index 0000000..b5959e9 --- /dev/null +++ b/Sources/Models/Topics.swift @@ -0,0 +1,91 @@ +public struct Topics { + + public var sensors: Sensors + public var setPoints: SetPoints + public var states: States + public var relays: Relays + + public init( + sensors: Sensors = .init(), + setPoints: SetPoints = .init(), + states: States = .init(), + relays: Relays = .init() + ) { + self.sensors = sensors + self.setPoints = setPoints + self.states = states + self.relays = relays + } + + public struct Sensors { + public var temperature: String + public var humidity: String + public var dewPoint: String + + public init( + temperature: String = "sensors/temperature", + humidity: String = "sensors/humidity", + dewPoint: String = "sensors/dew_point" + ) { + self.temperature = temperature + self.humidity = humidity + self.dewPoint = dewPoint + } + } + + public struct SetPoints { + public var humidify: String + public var dehumidify: Dehumidify + + public init( + humidify: String = "set_points/humidify", + dehumidify: Dehumidify = .init() + ) { + self.humidify = humidify + self.dehumidify = dehumidify + } + + public struct Dehumidify { + public var lowDewPoint: String + public var highDewPoint: String + public var lowRelativeHumidity: String + public var highRelativeHumidity: String + + public init( + lowDewPoint: String = "set_points/dehumidify/low_dew_point", + highDewPoint: String = "set_points/dehumidify/high_dew_point", + lowRelativeHumidity: String = "set_points/dehumidify/low_relative_humidity", + highRelativeHumidity: String = "set_points/dehumidify/high_relative_humidity" + ) { + self.lowDewPoint = lowDewPoint + self.highDewPoint = highDewPoint + self.lowRelativeHumidity = lowRelativeHumidity + self.highRelativeHumidity = highRelativeHumidity + } + } + } + + public struct States { + public var mode: String + + public init(mode: String = "states/mode") { + self.mode = mode + } + } + + public struct Relays { + public var dehumidification1: String + public var dehumidification2: String + public var humidification: String + + public init( + dehumidification1: String = "relays/dehumidification_1", + dehumidification2: String = "relays/dehumidification_2", + humidification: String = "relays/humidification" + ) { + self.dehumidification1 = dehumidification1 + self.dehumidification2 = dehumidification2 + self.humidification = humidification + } + } +} diff --git a/Sources/dewPoint-controller/main.swift b/Sources/dewPoint-controller/main.swift index 2e767ad..b87cbeb 100644 --- a/Sources/dewPoint-controller/main.swift +++ b/Sources/dewPoint-controller/main.swift @@ -1,46 +1,54 @@ import Bootstrap +import CoreUnitTypes import Logging import Models import MQTTNIO import NIO import Foundation -var logger = Logger(label: "dewPoint-logger") -logger.logLevel = .debug -logger.debug("Swift Dew Point Controller!") +var logger: Logger = { + var logger = Logger(label: "dewPoint-logger") + logger.logLevel = .info + return logger +}() + +logger.info("Starting Swift Dew Point Controller!") let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) let environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger).wait() -let relay = Relay(topic: "frankensystem/relays/switch/relay_1/command") -let tempSensor = TemperatureSensor(topic: "frankensystem/relays/sensor/temperature_-_1/state") -let humiditySensor = HumiditySensor(topic: "frankensystem/relays/sensor/humidity_-_1/state") + +// Set the log level to info only in production mode. +if environment.envVars.appEnv == .production { + logger.logLevel = .info +} + +let relay = Relay(topic: environment.topics.relays.dehumidification1) +let tempSensor = Sensor(topic: environment.topics.sensors.temperature) +let humiditySensor = Sensor(topic: environment.topics.sensors.humidity) defer { logger.debug("Disconnecting") - _ = try? environment.client.shutdown().wait() - try? environment.mqttClient.syncShutdownGracefully() + try? environment.mqttClient.shutdown().wait() } while true { -// logger.debug("Toggling relay.") -// _ = try environment.client.toggleRelay(relay).wait() - -// logger.debug("Reading temperature sensor.") -// let temp = try environment.client.fetchTemperature(tempSensor, .imperial).wait() -// logger.debug("Temperature: \(temp)") - -// logger.debug("Reading humidity sensor.") -// let humidity = try environment.client.fetchHumidity(humiditySensor).wait() -// logger.debug("Humdity: \(humidity)") logger.debug("Fetching dew point...") - let dp = try environment.client.fetchDewPoint( + + let dp = try environment.mqttClient.currentDewPoint( temperature: tempSensor, humidity: humiditySensor, - units: .imperial, - logger: logger + units: .imperial ).wait() - logger.debug("Dew Point: \(dp)") + + logger.info("Dew Point: \(dp.rawValue) \(dp.units.symbol)") + + try environment.mqttClient.publish( + dewPoint: dp, + to: environment.topics.sensors.dewPoint + ).wait() + + logger.debug("Published dew point...") Thread.sleep(forTimeInterval: 5) }