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

@@ -13,8 +13,8 @@ let package = Package(
.library(name: "DewPointEnvironment", targets: ["DewPointEnvironment"]), .library(name: "DewPointEnvironment", targets: ["DewPointEnvironment"]),
.library(name: "EnvVars", targets: ["EnvVars"]), .library(name: "EnvVars", targets: ["EnvVars"]),
.library(name: "Models", targets: ["Models"]), .library(name: "Models", targets: ["Models"]),
.library(name: "RelayClient", targets: ["RelayClient"]), .library(name: "Client", targets: ["Client"]),
.library(name: "TemperatureSensorClient", targets: ["TemperatureSensorClient"]), .library(name: "ClientLive", targets: ["ClientLive"]),
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/adam-fowler/mqtt-nio.git", from: "2.0.0"), .package(url: "https://github.com/adam-fowler/mqtt-nio.git", from: "2.0.0"),
@@ -39,8 +39,7 @@ let package = Package(
dependencies: [ dependencies: [
"DewPointEnvironment", "DewPointEnvironment",
"EnvVars", "EnvVars",
"RelayClient", "ClientLive",
"TemperatureSensorClient",
.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,8 +48,7 @@ let package = Package(
name: "DewPointEnvironment", name: "DewPointEnvironment",
dependencies: [ dependencies: [
"EnvVars", "EnvVars",
"RelayClient", "Client",
"TemperatureSensorClient",
.product(name: "MQTTNIO", package: "mqtt-nio"), .product(name: "MQTTNIO", package: "mqtt-nio"),
] ]
), ),
@@ -63,20 +61,19 @@ let package = Package(
dependencies: [] dependencies: []
), ),
.target( .target(
name: "RelayClient", name: "Client",
dependencies: [ dependencies: [
"Models", "Models",
.product(name: "CoreUnitTypes", package: "swift-psychrometrics"),
.product(name: "NIO", package: "swift-nio"), .product(name: "NIO", package: "swift-nio"),
.product(name: "MQTTNIO", package: "mqtt-nio") .product(name: "Psychrometrics", package: "swift-psychrometrics")
] ]
), ),
.target( .target(
name: "TemperatureSensorClient", name: "ClientLive",
dependencies: [ dependencies: [
"Models", "Client",
.product(name: "NIO", package: "swift-nio"), .product(name: "MQTTNIO", package: "mqtt-nio")
.product(name: "MQTTNIO", package: "mqtt-nio"),
.product(name: "CoreUnitTypes", package: "swift-psychrometrics")
] ]
), ),
] ]

View File

@@ -1,11 +1,10 @@
import ClientLive
import DewPointEnvironment import DewPointEnvironment
import EnvVars import EnvVars
import Logging import Logging
import Foundation import Foundation
import MQTTNIO import MQTTNIO
import NIO import NIO
import RelayClient
import TemperatureSensorClient
public func bootstrap( public func bootstrap(
eventLoopGroup: EventLoopGroup, eventLoopGroup: EventLoopGroup,
@@ -16,10 +15,9 @@ public func bootstrap(
.map { (envVars) -> DewPointEnvironment in .map { (envVars) -> DewPointEnvironment in
let mqttClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger) let mqttClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger)
return DewPointEnvironment.init( return DewPointEnvironment.init(
mqttClient: mqttClient, client: .live(client: mqttClient),
envVars: envVars, envVars: envVars,
relayClient: .live(client: mqttClient), mqttClient: mqttClient
temperatureSensorClient: .live(client: mqttClient)
) )
} }
.flatMap { environment in .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 EnvVars
import MQTTNIO import MQTTNIO
import RelayClient
import TemperatureSensorClient
public struct DewPointEnvironment { public struct DewPointEnvironment {
public var mqttClient: MQTTClient public var client: Client
public var envVars: EnvVars public var envVars: EnvVars
public var relayClient: RelayClient public var mqttClient: MQTTClient
public var temperatureSensorClient: TemperatureSensorClient
public init( public init(
mqttClient: MQTTClient, client: Client,
envVars: EnvVars, envVars: EnvVars,
relayClient: RelayClient, mqttClient: MQTTClient
temperatureSensorClient: TemperatureSensorClient
) { ) {
self.mqttClient = mqttClient self.mqttClient = mqttClient
self.envVars = envVars self.envVars = envVars
self.relayClient = relayClient self.client = client
self.temperatureSensorClient = temperatureSensorClient
} }
} }

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 eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger).wait() let environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger).wait()
let relayClient = environment.relayClient
let relay = Relay(topic: "frankensystem/relays/switch/relay_1/command") let relay = Relay(topic: "frankensystem/relays/switch/relay_1/command")
let tempSensor = TemperatureSensor(topic: "frankensystem/relays/sensor/temperature_-_1/state") let tempSensor = TemperatureSensor(topic: "frankensystem/relays/sensor/temperature_-_1/state")
let humiditySensor = HumiditySensor(topic: "frankensystem/relays/sensor/humidity_-_1/state")
defer { defer {
logger.debug("Disconnecting") logger.debug("Disconnecting")
_ = try? environment.mqttClient.disconnect().wait() _ = try? environment.client.shutdown().wait()
try? environment.mqttClient.syncShutdownGracefully() try? environment.mqttClient.syncShutdownGracefully()
} }
while true { while true {
logger.debug("Toggling relay.") // logger.debug("Toggling relay.")
_ = try relayClient.toggle(relay).wait() // _ = try environment.client.toggleRelay(relay).wait()
logger.debug("Reading temperature sensor.") // logger.debug("Reading temperature sensor.")
let temp = try environment.temperatureSensorClient.state(tempSensor, .imperial).wait() // let temp = try environment.client.fetchTemperature(tempSensor, .imperial).wait()
logger.debug("Temperature: \(temp)") // 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) Thread.sleep(forTimeInterval: 5)
} }