Cleaning up the api.
This commit is contained in:
@@ -4,5 +4,11 @@
|
|||||||
"MQTT_PORT": "1883",
|
"MQTT_PORT": "1883",
|
||||||
"MQTT_IDENTIFIER": "dewPoint-controller",
|
"MQTT_IDENTIFIER": "dewPoint-controller",
|
||||||
"MQTT_USERNAME": "mqtt_user",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ let package = Package(
|
|||||||
"DewPointEnvironment",
|
"DewPointEnvironment",
|
||||||
"EnvVars",
|
"EnvVars",
|
||||||
"ClientLive",
|
"ClientLive",
|
||||||
|
"Models",
|
||||||
.product(name: "MQTTNIO", package: "mqtt-nio"),
|
.product(name: "MQTTNIO", package: "mqtt-nio"),
|
||||||
.product(name: "NIO", package: "swift-nio")
|
.product(name: "NIO", package: "swift-nio")
|
||||||
]
|
]
|
||||||
@@ -49,6 +50,7 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
"EnvVars",
|
"EnvVars",
|
||||||
"Client",
|
"Client",
|
||||||
|
"Models",
|
||||||
.product(name: "MQTTNIO", package: "mqtt-nio"),
|
.product(name: "MQTTNIO", package: "mqtt-nio"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
@@ -58,7 +60,9 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "Models",
|
name: "Models",
|
||||||
dependencies: []
|
dependencies: [
|
||||||
|
.product(name: "CoreUnitTypes", package: "swift-psychrometrics"),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "Client",
|
name: "Client",
|
||||||
|
|||||||
@@ -3,30 +3,32 @@ import DewPointEnvironment
|
|||||||
import EnvVars
|
import EnvVars
|
||||||
import Logging
|
import Logging
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Models
|
||||||
import MQTTNIO
|
import MQTTNIO
|
||||||
import NIO
|
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(
|
public func bootstrap(
|
||||||
eventLoopGroup: EventLoopGroup,
|
eventLoopGroup: EventLoopGroup,
|
||||||
logger: Logger? = nil
|
logger: Logger? = nil
|
||||||
) -> EventLoopFuture<DewPointEnvironment> {
|
) -> EventLoopFuture<DewPointEnvironment> {
|
||||||
|
|
||||||
logger?.debug("Bootstrapping Dew Point Controller...")
|
logger?.debug("Bootstrapping Dew Point Controller...")
|
||||||
|
|
||||||
return loadEnvVars(eventLoopGroup: eventLoopGroup, logger: logger)
|
return loadEnvVars(eventLoopGroup: eventLoopGroup, logger: logger)
|
||||||
.map { (envVars) -> DewPointEnvironment in
|
.makeDewPointEnvironment(eventLoopGroup: eventLoopGroup, logger: logger)
|
||||||
let mqttClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger)
|
.connectToMQTTBroker(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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<EnvVars> {
|
private func loadEnvVars(eventLoopGroup: EventLoopGroup, logger: Logger?) -> EventLoopFuture<EnvVars> {
|
||||||
|
|
||||||
logger?.debug("Loading env vars...")
|
logger?.debug("Loading env vars...")
|
||||||
@@ -66,6 +68,49 @@ private func loadEnvVars(eventLoopGroup: EventLoopGroup, logger: Logger?) -> Eve
|
|||||||
return eventLoopGroup.next().makeSucceededFuture(envVars)
|
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<DewPointEnvironment> {
|
||||||
|
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<DewPointEnvironment> {
|
||||||
|
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 {
|
extension MQTTClient {
|
||||||
|
|
||||||
convenience init(envVars: EnvVars, eventLoopGroup: EventLoopGroup, logger: Logger?) {
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,42 +5,80 @@ import Models
|
|||||||
import NIO
|
import NIO
|
||||||
import Psychrometrics
|
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<RelativeHumidity>
|
/// Retrieve the humidity from the MQTT Broker.
|
||||||
public var fetchTemperature: (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
|
public var fetchHumidity: (Sensor<RelativeHumidity>) -> EventLoopFuture<RelativeHumidity>
|
||||||
public var toggleRelay: (Relay) -> EventLoopFuture<Void>
|
|
||||||
public var turnOnRelay: (Relay) -> EventLoopFuture<Void>
|
/// Retrieve the temperature from the MQTT Broker.
|
||||||
public var turnOffRelay: (Relay) -> EventLoopFuture<Void>
|
public var fetchTemperature: (Sensor<Temperature>, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
|
||||||
|
|
||||||
|
/// Publish a change of state message for a relay.
|
||||||
|
public var setRelay: (Relay, Relay.State) -> EventLoopFuture<Void>
|
||||||
|
|
||||||
|
/// Disconnect and close the connection to the MQTT Broker.
|
||||||
public var shutdown: () -> EventLoopFuture<Void>
|
public var shutdown: () -> EventLoopFuture<Void>
|
||||||
|
|
||||||
|
/// Publish the current dew point to the MQTT Broker
|
||||||
|
public var publishDewPoint: (DewPoint, String) -> EventLoopFuture<Void>
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
fetchHumidity: @escaping (HumiditySensor) -> EventLoopFuture<RelativeHumidity>,
|
fetchHumidity: @escaping (Sensor<RelativeHumidity>) -> EventLoopFuture<RelativeHumidity>,
|
||||||
fetchTemperature: @escaping (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>,
|
fetchTemperature: @escaping (Sensor<Temperature>, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>,
|
||||||
toggleRelay: @escaping (Relay) -> EventLoopFuture<Void>,
|
setRelay: @escaping (Relay, Relay.State) -> EventLoopFuture<Void>,
|
||||||
turnOnRelay: @escaping (Relay) -> EventLoopFuture<Void>,
|
shutdown: @escaping () -> EventLoopFuture<Void>,
|
||||||
turnOffRelay: @escaping (Relay) -> EventLoopFuture<Void>,
|
publishDewPoint: @escaping (DewPoint, String) -> EventLoopFuture<Void>
|
||||||
shutdown: @escaping () -> EventLoopFuture<Void>
|
|
||||||
) {
|
) {
|
||||||
self.fetchHumidity = fetchHumidity
|
self.fetchHumidity = fetchHumidity
|
||||||
self.fetchTemperature = fetchTemperature
|
self.fetchTemperature = fetchTemperature
|
||||||
self.toggleRelay = toggleRelay
|
self.setRelay = setRelay
|
||||||
self.turnOnRelay = turnOnRelay
|
|
||||||
self.turnOffRelay = turnOffRelay
|
|
||||||
self.shutdown = shutdown
|
self.shutdown = shutdown
|
||||||
|
self.publishDewPoint = publishDewPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchDewPoint(
|
/// Fetches the current temperature and humidity and calculates the current dew point.
|
||||||
temperature: TemperatureSensor,
|
///
|
||||||
humidity: HumiditySensor,
|
/// - Parameters:
|
||||||
units: PsychrometricEnvironment.Units? = nil,
|
/// - temperature: The temperature sensor to fetch the temperature from.
|
||||||
logger: Logger? = nil
|
/// - humidity: The humidity sensor to fetch the humidity from.
|
||||||
|
/// - units: Optional units for the dew point.
|
||||||
|
public func currentDewPoint(
|
||||||
|
temperature: Sensor<Temperature>,
|
||||||
|
humidity: Sensor<RelativeHumidity>,
|
||||||
|
units: PsychrometricEnvironment.Units? = nil
|
||||||
) -> EventLoopFuture<DewPoint> {
|
) -> EventLoopFuture<DewPoint> {
|
||||||
fetchTemperature(temperature, units)
|
fetchTemperature(temperature, units)
|
||||||
.and(fetchHumidity(humidity))
|
.and(fetchHumidity(humidity))
|
||||||
.map { temp, humidity in
|
.convertToDewPoint(units: units)
|
||||||
logger?.debug("Creating dew-point for temperature: \(temp) with humidity: \(humidity)")
|
}
|
||||||
return DewPoint.init(dryBulb: temp, humidity: humidity, 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<Void> {
|
||||||
|
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<Void> {
|
||||||
|
publishDewPoint(dewPoint, topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EventLoopFuture where Value == (Temperature, RelativeHumidity) {
|
||||||
|
|
||||||
|
fileprivate func convertToDewPoint(units: PsychrometricEnvironment.Units?) -> EventLoopFuture<DewPoint> {
|
||||||
|
map { .init(dryBulb: $0, humidity: $1, units: units) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
Sources/ClientLive/Helpers.swift
Normal file
139
Sources/ClientLive/Helpers.swift
Normal file
@@ -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<S>(
|
||||||
|
sensor: Sensor<S>
|
||||||
|
) -> EventLoopFuture<S> 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<Void> {
|
||||||
|
publish(
|
||||||
|
to: relay.topic,
|
||||||
|
payload: ByteBufferAllocator().buffer(string: state.rawValue),
|
||||||
|
qos: qos
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Result where Success == MQTTPublishInfo, Failure == Error {
|
||||||
|
|
||||||
|
func mapBuffer<S>(to type: S.Type) -> Result<S?, Error> where S: BufferInitalizable {
|
||||||
|
map { info in
|
||||||
|
var buffer = info.payload
|
||||||
|
return S.init(buffer: &buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Result {
|
||||||
|
|
||||||
|
func fullfill(promise: EventLoopPromise<Success>) {
|
||||||
|
switch self {
|
||||||
|
case let.success(value):
|
||||||
|
promise.succeed(value)
|
||||||
|
case let .failure(error):
|
||||||
|
promise.fail(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Result where Failure == Error {
|
||||||
|
|
||||||
|
func unwrap<S, F>(
|
||||||
|
or error: @autoclosure @escaping () -> F
|
||||||
|
) -> Result<S, Error> where Success == Optional<S>, 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<Temperature> {
|
||||||
|
map { currentTemperature in
|
||||||
|
guard let units = units else { return currentTemperature }
|
||||||
|
return currentTemperature.convert(to: units)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,135 +5,35 @@ import Models
|
|||||||
import MQTTNIO
|
import MQTTNIO
|
||||||
import NIO
|
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(
|
.init(
|
||||||
fetchHumidity: { sensor in
|
fetchHumidity: { sensor in
|
||||||
client.fetchHumidity(sensor: sensor)
|
client.fetch(sensor: sensor)
|
||||||
},
|
},
|
||||||
fetchTemperature: { sensor, units in
|
fetchTemperature: { sensor, units in
|
||||||
client.fetchTemperature(sensor: sensor, units: units)
|
client.fetch(sensor: sensor)
|
||||||
|
.convertIfNeeded(to: units)
|
||||||
},
|
},
|
||||||
toggleRelay: { relay in
|
setRelay: { relay, state in
|
||||||
client.publish(relay: relay, state: .toggle, qos: .atLeastOnce)
|
client.set(relay: relay, to: state)
|
||||||
},
|
|
||||||
turnOnRelay: { relay in
|
|
||||||
client.publish(relay: relay, state: .on, qos: .atLeastOnce)
|
|
||||||
},
|
|
||||||
turnOffRelay: { relay in
|
|
||||||
client.publish(relay: relay, state: .off, qos: .atLeastOnce)
|
|
||||||
},
|
},
|
||||||
shutdown: {
|
shutdown: {
|
||||||
client.disconnect()
|
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<Void> {
|
|
||||||
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<Temperature> {
|
|
||||||
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<RelativeHumidity> {
|
|
||||||
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<RelativeHumidity, Error> {
|
|
||||||
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<Temperature, Error> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import Client
|
import Client
|
||||||
import EnvVars
|
import EnvVars
|
||||||
|
import Models
|
||||||
import MQTTNIO
|
import MQTTNIO
|
||||||
|
|
||||||
public struct DewPointEnvironment {
|
public struct DewPointEnvironment {
|
||||||
|
|
||||||
public var client: Client
|
public var mqttClient: Client.MQTTClient
|
||||||
public var envVars: EnvVars
|
public var envVars: EnvVars
|
||||||
public var mqttClient: MQTTClient
|
public var nioClient: MQTTNIO.MQTTClient
|
||||||
|
public var topics: Topics
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
client: Client,
|
mqttClient: Client.MQTTClient,
|
||||||
envVars: EnvVars,
|
envVars: EnvVars,
|
||||||
mqttClient: MQTTClient
|
nioClient: MQTTNIO.MQTTClient,
|
||||||
|
topics: Topics = .init()
|
||||||
) {
|
) {
|
||||||
self.mqttClient = mqttClient
|
self.mqttClient = mqttClient
|
||||||
self.envVars = envVars
|
self.envVars = envVars
|
||||||
self.client = client
|
self.nioClient = nioClient
|
||||||
|
self.topics = topics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ public struct EnvVars: Codable, Equatable {
|
|||||||
/// The MQTT user password.
|
/// The MQTT user password.
|
||||||
public var password: String?
|
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``
|
/// Create a new ``EnvVars``
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -39,7 +48,13 @@ public struct EnvVars: Codable, Equatable {
|
|||||||
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!",
|
||||||
|
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.appEnv = appEnv
|
||||||
self.host = host
|
self.host = host
|
||||||
@@ -47,6 +62,12 @@ public struct EnvVars: Codable, Equatable {
|
|||||||
self.identifier = identifier
|
self.identifier = identifier
|
||||||
self.userName = userName
|
self.userName = userName
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.dehumidificationStage1Relay = dehumidificationStage1Relay
|
||||||
|
self.dehumidificationStage2Relay = dehumidificationStage2Relay
|
||||||
|
self.dewPointTopic = dewPointTopic
|
||||||
|
self.humidificationRelay = humidificationRelay
|
||||||
|
self.humiditySensor = humiditySensor
|
||||||
|
self.temperatureSensor = temperatureSensor
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom coding keys.
|
/// Custom coding keys.
|
||||||
@@ -57,6 +78,12 @@ public struct EnvVars: Codable, Equatable {
|
|||||||
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 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.
|
/// Represents the different app environments.
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
public struct HumiditySensor: Equatable {
|
|
||||||
public var topic: String
|
|
||||||
|
|
||||||
public init(topic: String) {
|
|
||||||
self.topic = topic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
Sources/Models/Mode.swift
Normal file
37
Sources/Models/Mode.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,40 @@
|
|||||||
|
|
||||||
|
/// Represents a relay that can be controlled by the MQTT Broker.
|
||||||
public struct Relay {
|
public struct Relay {
|
||||||
|
|
||||||
|
/// The topic for the relay.
|
||||||
public var topic: String
|
public var topic: String
|
||||||
|
|
||||||
|
/// Create a new relay at the given topic.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - topic: The topic for commanding the relay.
|
||||||
public init(topic: String) {
|
public init(topic: String) {
|
||||||
self.topic = topic
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
15
Sources/Models/Sensor.swift
Normal file
15
Sources/Models/Sensor.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
/// Represents a sensor that provides a reading.
|
||||||
|
public struct Sensor<Reading>: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
public struct TemperatureSensor: Equatable {
|
|
||||||
public var topic: String
|
|
||||||
|
|
||||||
public init(topic: String) {
|
|
||||||
self.topic = topic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
91
Sources/Models/Topics.swift
Normal file
91
Sources/Models/Topics.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,54 @@
|
|||||||
import Bootstrap
|
import Bootstrap
|
||||||
|
import CoreUnitTypes
|
||||||
import Logging
|
import Logging
|
||||||
import Models
|
import Models
|
||||||
import MQTTNIO
|
import MQTTNIO
|
||||||
import NIO
|
import NIO
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
var logger = Logger(label: "dewPoint-logger")
|
var logger: Logger = {
|
||||||
logger.logLevel = .debug
|
var logger = Logger(label: "dewPoint-logger")
|
||||||
logger.debug("Swift Dew Point Controller!")
|
logger.logLevel = .info
|
||||||
|
return logger
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.info("Starting Swift Dew Point Controller!")
|
||||||
|
|
||||||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||||
let environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger).wait()
|
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")
|
// Set the log level to info only in production mode.
|
||||||
let humiditySensor = HumiditySensor(topic: "frankensystem/relays/sensor/humidity_-_1/state")
|
if environment.envVars.appEnv == .production {
|
||||||
|
logger.logLevel = .info
|
||||||
|
}
|
||||||
|
|
||||||
|
let relay = Relay(topic: environment.topics.relays.dehumidification1)
|
||||||
|
let tempSensor = Sensor<Temperature>(topic: environment.topics.sensors.temperature)
|
||||||
|
let humiditySensor = Sensor<RelativeHumidity>(topic: environment.topics.sensors.humidity)
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
logger.debug("Disconnecting")
|
logger.debug("Disconnecting")
|
||||||
_ = try? environment.client.shutdown().wait()
|
try? environment.mqttClient.shutdown().wait()
|
||||||
try? environment.mqttClient.syncShutdownGracefully()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while true {
|
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...")
|
logger.debug("Fetching dew point...")
|
||||||
let dp = try environment.client.fetchDewPoint(
|
|
||||||
|
let dp = try environment.mqttClient.currentDewPoint(
|
||||||
temperature: tempSensor,
|
temperature: tempSensor,
|
||||||
humidity: humiditySensor,
|
humidity: humiditySensor,
|
||||||
units: .imperial,
|
units: .imperial
|
||||||
logger: logger
|
|
||||||
).wait()
|
).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)
|
Thread.sleep(forTimeInterval: 5)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user