Reading / calculating dew-point.

This commit is contained in:
2021-10-16 16:37:35 -04:00
parent 42c46d9d84
commit 222bb61103
10 changed files with 222 additions and 157 deletions

View File

@@ -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

View File

@@ -0,0 +1,46 @@
import CoreUnitTypes
import Logging
import Foundation
import Models
import NIO
import Psychrometrics
public struct Client {
public var fetchHumidity: (HumiditySensor) -> EventLoopFuture<RelativeHumidity>
public var fetchTemperature: (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
public var toggleRelay: (Relay) -> EventLoopFuture<Void>
public var turnOnRelay: (Relay) -> EventLoopFuture<Void>
public var turnOffRelay: (Relay) -> EventLoopFuture<Void>
public var shutdown: () -> EventLoopFuture<Void>
public init(
fetchHumidity: @escaping (HumiditySensor) -> EventLoopFuture<RelativeHumidity>,
fetchTemperature: @escaping (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>,
toggleRelay: @escaping (Relay) -> EventLoopFuture<Void>,
turnOnRelay: @escaping (Relay) -> EventLoopFuture<Void>,
turnOffRelay: @escaping (Relay) -> EventLoopFuture<Void>,
shutdown: @escaping () -> EventLoopFuture<Void>
) {
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<DewPoint> {
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)
}
}
}

View File

@@ -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<Void> {
publish(
to: relay.topic,
payload: ByteBufferAllocator().buffer(string: state.rawValue),
qos: qos
)
}
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
}
}
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))
}
}
}

View File

@@ -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
}
}

View File

@@ -1,20 +0,0 @@
import Foundation
import Models
import NIO
public struct RelayClient {
public var toggle: (Relay) -> EventLoopFuture<Void>
public var turnOn: (Relay) -> EventLoopFuture<Void>
public var turnOff: (Relay) -> EventLoopFuture<Void>
public init(
toggle: @escaping (Relay) -> EventLoopFuture<Void>,
turnOn: @escaping (Relay) -> EventLoopFuture<Void>,
turnOff: @escaping (Relay) -> EventLoopFuture<Void>
) {
self.toggle = toggle
self.turnOn = turnOn
self.turnOff = turnOff
}
}

View File

@@ -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<Void> {
publish(
to: relay.topic,
payload: ByteBufferAllocator().buffer(string: state.rawValue),
qos: qos
)
}
}

View File

@@ -1,14 +0,0 @@
import Foundation
import CoreUnitTypes
import Models
import NIO
public struct TemperatureSensorClient {
public var state: (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
public init(
state: @escaping (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
) {
self.state = state
}
}

View File

@@ -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<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))
}
}
}

View File

@@ -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)
}