diff --git a/Package.swift b/Package.swift index a2e6743..0869a86 100644 --- a/Package.swift +++ b/Package.swift @@ -13,8 +13,8 @@ let package = Package( .library(name: "DewPointEnvironment", targets: ["DewPointEnvironment"]), .library(name: "EnvVars", targets: ["EnvVars"]), .library(name: "Models", targets: ["Models"]), - .library(name: "RelayClient", targets: ["RelayClient"]), - .library(name: "TemperatureSensorClient", targets: ["TemperatureSensorClient"]), + .library(name: "Client", targets: ["Client"]), + .library(name: "ClientLive", targets: ["ClientLive"]), ], dependencies: [ .package(url: "https://github.com/adam-fowler/mqtt-nio.git", from: "2.0.0"), @@ -39,8 +39,7 @@ let package = Package( dependencies: [ "DewPointEnvironment", "EnvVars", - "RelayClient", - "TemperatureSensorClient", + "ClientLive", .product(name: "MQTTNIO", package: "mqtt-nio"), .product(name: "NIO", package: "swift-nio") ] @@ -49,8 +48,7 @@ let package = Package( name: "DewPointEnvironment", dependencies: [ "EnvVars", - "RelayClient", - "TemperatureSensorClient", + "Client", .product(name: "MQTTNIO", package: "mqtt-nio"), ] ), @@ -63,20 +61,19 @@ let package = Package( dependencies: [] ), .target( - name: "RelayClient", + name: "Client", dependencies: [ "Models", + .product(name: "CoreUnitTypes", package: "swift-psychrometrics"), .product(name: "NIO", package: "swift-nio"), - .product(name: "MQTTNIO", package: "mqtt-nio") + .product(name: "Psychrometrics", package: "swift-psychrometrics") ] ), .target( - name: "TemperatureSensorClient", + name: "ClientLive", dependencies: [ - "Models", - .product(name: "NIO", package: "swift-nio"), - .product(name: "MQTTNIO", package: "mqtt-nio"), - .product(name: "CoreUnitTypes", package: "swift-psychrometrics") + "Client", + .product(name: "MQTTNIO", package: "mqtt-nio") ] ), ] diff --git a/Sources/Bootstrap/Bootstrap.swift b/Sources/Bootstrap/Bootstrap.swift index 4cee1d2..08026c2 100644 --- a/Sources/Bootstrap/Bootstrap.swift +++ b/Sources/Bootstrap/Bootstrap.swift @@ -1,11 +1,10 @@ +import ClientLive import DewPointEnvironment import EnvVars import Logging import Foundation import MQTTNIO import NIO -import RelayClient -import TemperatureSensorClient public func bootstrap( eventLoopGroup: EventLoopGroup, @@ -16,10 +15,9 @@ public func bootstrap( .map { (envVars) -> DewPointEnvironment in let mqttClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger) return DewPointEnvironment.init( - mqttClient: mqttClient, + client: .live(client: mqttClient), envVars: envVars, - relayClient: .live(client: mqttClient), - temperatureSensorClient: .live(client: mqttClient) + mqttClient: mqttClient ) } .flatMap { environment in diff --git a/Sources/Client/Interface.swift b/Sources/Client/Interface.swift new file mode 100644 index 0000000..7d0aade --- /dev/null +++ b/Sources/Client/Interface.swift @@ -0,0 +1,46 @@ +import CoreUnitTypes +import Logging +import Foundation +import Models +import NIO +import Psychrometrics + +public struct Client { + + 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 + public var shutdown: () -> 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 + ) { + self.fetchHumidity = fetchHumidity + self.fetchTemperature = fetchTemperature + self.toggleRelay = toggleRelay + self.turnOnRelay = turnOnRelay + self.turnOffRelay = turnOffRelay + self.shutdown = shutdown + } + + public func fetchDewPoint( + temperature: TemperatureSensor, + humidity: HumiditySensor, + units: PsychrometricEnvironment.Units? = nil, + logger: Logger? = 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) + } + } +} diff --git a/Sources/ClientLive/Live.swift b/Sources/ClientLive/Live.swift new file mode 100644 index 0000000..f565a2a --- /dev/null +++ b/Sources/ClientLive/Live.swift @@ -0,0 +1,137 @@ +import Foundation +import Client +import CoreUnitTypes +import Models +import MQTTNIO +import NIO + +extension Client { + + public static func live(client: MQTTClient) -> Self { + .init( + fetchHumidity: { sensor in + client.fetchHumidity(sensor: sensor) + }, + fetchTemperature: { sensor, units in + client.fetchTemperature(sensor: sensor, units: 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) + }, + shutdown: { + client.disconnect() + } + ) + } +} + +// 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 + ) + } + + 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 + } + } + + 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 ae5cc5c..4c8f11e 100644 --- a/Sources/DewPointEnvironment/Environment.swift +++ b/Sources/DewPointEnvironment/Environment.swift @@ -1,24 +1,20 @@ +import Client import EnvVars import MQTTNIO -import RelayClient -import TemperatureSensorClient public struct DewPointEnvironment { - public var mqttClient: MQTTClient + public var client: Client public var envVars: EnvVars - public var relayClient: RelayClient - public var temperatureSensorClient: TemperatureSensorClient + public var mqttClient: MQTTClient public init( - mqttClient: MQTTClient, + client: Client, envVars: EnvVars, - relayClient: RelayClient, - temperatureSensorClient: TemperatureSensorClient + mqttClient: MQTTClient ) { self.mqttClient = mqttClient self.envVars = envVars - self.relayClient = relayClient - self.temperatureSensorClient = temperatureSensorClient + self.client = client } } diff --git a/Sources/RelayClient/Interface.swift b/Sources/RelayClient/Interface.swift deleted file mode 100644 index ceb1373..0000000 --- a/Sources/RelayClient/Interface.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import Models -import NIO - -public struct RelayClient { - public var toggle: (Relay) -> EventLoopFuture - public var turnOn: (Relay) -> EventLoopFuture - public var turnOff: (Relay) -> EventLoopFuture - - public init( - toggle: @escaping (Relay) -> EventLoopFuture, - turnOn: @escaping (Relay) -> EventLoopFuture, - turnOff: @escaping (Relay) -> EventLoopFuture - ) { - self.toggle = toggle - self.turnOn = turnOn - self.turnOff = turnOff - } -} - diff --git a/Sources/RelayClient/Live.swift b/Sources/RelayClient/Live.swift deleted file mode 100644 index 0b322a0..0000000 --- a/Sources/RelayClient/Live.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Models -import MQTTNIO -import NIO - -extension RelayClient { - - public static func live(client: MQTTClient) -> RelayClient { - .init( - toggle: { relay in - client.publish(relay: relay, state: .toggle) - }, - turnOn: { relay in - client.publish(relay: relay, state: .on) - }, - turnOff: { relay in - client.publish(relay: relay, state: .off) - } - ) - } -} - -extension Relay { - enum State: String { - case toggle, on, off - } -} - -extension MQTTClient { - - func publish(relay: Relay, state: Relay.State, qos: MQTTQoS = .atLeastOnce) -> EventLoopFuture { - publish( - to: relay.topic, - payload: ByteBufferAllocator().buffer(string: state.rawValue), - qos: qos - ) - } -} diff --git a/Sources/TemperatureSensorClient/Interface.swift b/Sources/TemperatureSensorClient/Interface.swift deleted file mode 100644 index 5783ea4..0000000 --- a/Sources/TemperatureSensorClient/Interface.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import CoreUnitTypes -import Models -import NIO - -public struct TemperatureSensorClient { - public var state: (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture - - public init( - state: @escaping (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture - ) { - self.state = state - } -} diff --git a/Sources/TemperatureSensorClient/Live.swift b/Sources/TemperatureSensorClient/Live.swift deleted file mode 100644 index aaee961..0000000 --- a/Sources/TemperatureSensorClient/Live.swift +++ /dev/null @@ -1,51 +0,0 @@ -import CoreUnitTypes -import Foundation -import MQTTNIO - -extension TemperatureSensorClient { - - public static func live(client: MQTTClient) -> TemperatureSensorClient { - .init( - state: { sensor, units in - client.logger.debug("Adding listener for temperature sensor...") - let subscription = MQTTSubscribeInfoV5.init(topicFilter: sensor.topic, qos: .atLeastOnce) - return client.v5.subscribe(to: [subscription]) - .flatMap { _ in - let promise = client.eventLoopGroup.next().makePromise(of: Temperature.self) - client.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 - } - } - ) - } -} - -public enum TemperatureError: Error { - case invalidTemperature -} - -// MARK: - Helpers -extension Result where Success == MQTTPublishInfo, Failure == Error { - - 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/dewPoint-controller/main.swift b/Sources/dewPoint-controller/main.swift index dea1907..d624eb6 100644 --- a/Sources/dewPoint-controller/main.swift +++ b/Sources/dewPoint-controller/main.swift @@ -12,23 +12,36 @@ logger.debug("Swift Dew Point Controller!") let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) let environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger).wait() -let relayClient = environment.relayClient 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") defer { logger.debug("Disconnecting") - _ = try? environment.mqttClient.disconnect().wait() + _ = try? environment.client.shutdown().wait() try? environment.mqttClient.syncShutdownGracefully() } while true { - logger.debug("Toggling relay.") - _ = try relayClient.toggle(relay).wait() +// logger.debug("Toggling relay.") +// _ = try environment.client.toggleRelay(relay).wait() - logger.debug("Reading temperature sensor.") - let temp = try environment.temperatureSensorClient.state(tempSensor, .imperial).wait() - logger.debug("Temperature: \(temp)") +// 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( + temperature: tempSensor, + humidity: humiditySensor, + units: .imperial, + logger: logger + ).wait() + logger.debug("Dew Point: \(dp)") Thread.sleep(forTimeInterval: 5) }