Minimal working version publishing dew-points and enthalpies.

This commit is contained in:
2021-11-05 16:59:25 -04:00
parent 31eaa6ade1
commit 2c255b6510
7 changed files with 128 additions and 46 deletions

View File

@@ -5,6 +5,8 @@ bootstrap-env:
bootstrap-topics: bootstrap-topics:
@cp Bootstrap/topics-example .topics @cp Bootstrap/topics-example .topics
bootstrap: bootstrap-env bootstrap-topics
build: build:
@swift build @swift build

View File

@@ -26,6 +26,8 @@ let package = Package(
name: "dewPoint-controller", name: "dewPoint-controller",
dependencies: [ dependencies: [
"Bootstrap", "Bootstrap",
"ClientLive",
"TopicsLive",
.product(name: "MQTTNIO", package: "mqtt-nio"), .product(name: "MQTTNIO", package: "mqtt-nio"),
.product(name: "NIO", package: "swift-nio") .product(name: "NIO", package: "swift-nio")
] ]
@@ -87,5 +89,11 @@ let package = Package(
"ClientLive" "ClientLive"
] ]
), ),
.target(
name: "TopicsLive",
dependencies: [
"Models"
]
),
] ]
) )

View File

@@ -10,32 +10,34 @@ protocol BufferInitalizable {
init?(buffer: inout ByteBuffer) init?(buffer: inout ByteBuffer)
} }
extension Temperature: BufferInitalizable { extension Double: BufferInitalizable {
/// Attempt to create / parse a double from a byte buffer.
init?(buffer: inout ByteBuffer) { init?(buffer: inout ByteBuffer) {
guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8), guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8)
let value = Double(string) else { return nil }
else { self.init(string)
return nil
} }
}
extension Temperature: BufferInitalizable {
/// Attempt to create / parse a temperature from a byte buffer.
init?(buffer: inout ByteBuffer) {
guard let value = Double(buffer: &buffer) else { return nil }
self.init(value, units: .celsius) self.init(value, units: .celsius)
} }
} }
extension RelativeHumidity: BufferInitalizable { extension RelativeHumidity: BufferInitalizable {
/// Attempt to create / parse a relative humidity from a byte buffer.
init?(buffer: inout ByteBuffer) { init?(buffer: inout ByteBuffer) {
guard let string = buffer.readString(length: buffer.readableBytes, encoding: .utf8), guard let value = Double(buffer: &buffer) else { return nil }
let value = Double(string)
else {
return nil
}
self.init(value) self.init(value)
} }
} }
extension MQTTNIO.MQTTClient { extension MQTTNIO.MQTTClient {
/// Logs a failure for a given topic and error.
func logFailure(topic: String, error: Error) { func logFailure(topic: String, error: Error) {
logger.error("\(topic): \(error)") logger.error("\(topic): \(error)")
} }
@@ -214,10 +216,11 @@ extension MQTTNIO.MQTTClient {
logger.trace("No dew point for sensor.") logger.trace("No dew point for sensor.")
return eventLoopGroup.next().makeSucceededFuture((self, request, state, topics)) return eventLoopGroup.next().makeSucceededFuture((self, request, state, topics))
} }
let roundedDewPoint = round(dewPoint.rawValue * 100) / 100
logger.debug("Publishing dew-point: \(dewPoint), to: \(topic)") logger.debug("Publishing dew-point: \(dewPoint), to: \(topic)")
return publish( return publish(
to: topic, to: topic,
payload: ByteBufferAllocator().buffer(string: "\(dewPoint.rawValue)"), payload: ByteBufferAllocator().buffer(string: "\(roundedDewPoint)"),
qos: .atLeastOnce qos: .atLeastOnce
) )
.map { (self, request, state, topics) } .map { (self, request, state, topics) }
@@ -240,10 +243,11 @@ extension EventLoopFuture where Value == (MQTTNIO.MQTTClient, Client.SensorPubli
client.logger.trace("No enthalpy for sensor.") client.logger.trace("No enthalpy for sensor.")
return client.eventLoopGroup.next().makeSucceededFuture((request, state)) return client.eventLoopGroup.next().makeSucceededFuture((request, state))
} }
let roundedEnthalpy = round(enthalpy.rawValue * 100) / 100
client.logger.debug("Publishing enthalpy: \(enthalpy), to: \(topic)") client.logger.debug("Publishing enthalpy: \(enthalpy), to: \(topic)")
return client.publish( return client.publish(
to: topic, to: topic,
payload: ByteBufferAllocator().buffer(string: "\(enthalpy.rawValue)"), payload: ByteBufferAllocator().buffer(string: "\(roundedEnthalpy)"),
qos: .atLeastOnce qos: .atLeastOnce
) )
.map { (request, state) } .map { (request, state) }

View File

@@ -70,14 +70,18 @@ extension State.Sensors {
public func dewPoint(units: PsychrometricEnvironment.Units? = nil) -> DewPoint? { public func dewPoint(units: PsychrometricEnvironment.Units? = nil) -> DewPoint? {
guard let temperature = temperature, guard let temperature = temperature,
let humidity = humidity let humidity = humidity,
!temperature.rawValue.isNaN,
!humidity.rawValue.isNaN
else { return nil } else { return nil }
return .init(dryBulb: temperature, humidity: humidity, units: units) return .init(dryBulb: temperature, humidity: humidity, units: units)
} }
public func enthalpy(altitude: Length, units: PsychrometricEnvironment.Units? = nil) -> EnthalpyOf<MoistAir>? { public func enthalpy(altitude: Length, units: PsychrometricEnvironment.Units? = nil) -> EnthalpyOf<MoistAir>? {
guard let temperature = temperature, guard let temperature = temperature,
let humidity = humidity let humidity = humidity,
!temperature.rawValue.isNaN,
!humidity.rawValue.isNaN
else { return nil } else { return nil }
return .init(dryBulb: temperature, humidity: humidity, altitude: altitude, units: units) return .init(dryBulb: temperature, humidity: humidity, altitude: altitude, units: units)
} }

View File

@@ -0,0 +1,52 @@
import Models
// TODO: Fix other live topics
extension Topics {
public static let live = Self.init(
commands: .init(),
sensors: .init(
mixedAirSensor: .live(location: .mixedAir),
postCoilSensor: .live(location: .postCoil),
returnAirSensor: .live(location: .return),
supplyAirSensor: .live(location: .supply)),
setPoints: .init(),
states: .init()
)
}
extension Topics.Sensors {
fileprivate enum Location: CustomStringConvertible {
case mixedAir
case postCoil
case `return`
case supply
var description: String {
switch self {
case .mixedAir:
return "mixed_air"
case .postCoil:
return "post_coil"
case .return:
return "return"
case .supply:
return "supply"
}
}
}
}
extension Topics.Sensors.TemperatureAndHumiditySensor {
fileprivate static func live(
prefix: String = "frankensystem",
location: Topics.Sensors.Location
) -> Self {
.init(
temperature: "\(prefix)/sensor/\(location.description)_temperature/state",
humidity: "\(prefix)/sensor/\(location.description)_humidity/state",
dewPoint: "\(prefix)/sensor/\(location.description)_dew_point/state",
enthalpy: "\(prefix)/sensor/\(location.description)_enthalpy/state"
)
}
}

View File

@@ -1,60 +1,73 @@
import Bootstrap import Bootstrap
import ClientLive
import CoreUnitTypes import CoreUnitTypes
import Logging import Logging
import Models import Models
import MQTTNIO import MQTTNIO
import NIO import NIO
import TopicsLive
import Foundation import Foundation
var logger: Logger = { var logger: Logger = {
var logger = Logger(label: "dewPoint-logger") var logger = Logger(label: "dewPoint-logger")
logger.logLevel = .info logger.logLevel = .debug
return logger return logger
}() }()
logger.info("Starting Swift Dew Point Controller!") 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() var environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger, autoConnect: false).wait()
// Set the log level to info only in production mode. // Set the log level to info only in production mode.
if environment.envVars.appEnv == .production { if environment.envVars.appEnv == .production {
logger.logLevel = .info logger.logLevel = .info
} }
//let relay = Relay(topic: environment.topics.commands.relays.dehumidification1) // Set up the client, topics and state.
//let tempSensor = Sensor<Temperature>(topic: environment.topics.sensors.returnAirSensor.temperature) environment.topics = .live
//let humiditySensor = Sensor<RelativeHumidity>(topic: environment.topics.sensors.returnAirSensor.humidity) let state = State()
let client = Client.live(client: environment.mqttClient, state: state, topics: environment.topics)
defer { defer {
logger.debug("Disconnecting") logger.debug("Disconnecting")
// try? environment.mqttClient.shutdown().wait()
} }
// Add topic listeners.
client.addListeners()
while true { while true {
// let temp = try environment.mqttClient.fetchTemperature(tempSensor, .imperial).wait() if !environment.mqttClient.isActive() {
// logger.debug("Temp: \(temp.rawValue)") logger.trace("Connecting to MQTT broker...")
try client.connect().wait()
try client.subscribe().wait()
Thread.sleep(forTimeInterval: 1)
}
// Check if sensors need processed.
if state.sensors.needsProcessed {
logger.debug("Sensor state has changed...")
if state.sensors.mixedAirSensor.needsProcessed {
logger.trace("Publishing mixed air sensor.")
try client.publishSensor(.mixed(state.sensors.mixedAirSensor)).wait()
}
if state.sensors.postCoilSensor.needsProcessed {
logger.trace("Publishing post coil sensor.")
try client.publishSensor(.postCoil(state.sensors.postCoilSensor)).wait()
}
if state.sensors.returnAirSensor.needsProcessed {
logger.trace("Publishing return air sensor.")
try client.publishSensor(.return(state.sensors.returnAirSensor)).wait()
}
if state.sensors.supplyAirSensor.needsProcessed {
logger.trace("Publishing supply air sensor.")
try client.publishSensor(.supply(state.sensors.supplyAirSensor)).wait()
}
}
// logger.debug("Fetching dew point...")
// //
// logger.debug("Fetching set-point...") // logger.debug("Published dew point...")
// let sp = try environment.mqttClient.fetchSetPoint(\.dehumidify.highDewPoint).wait()
// logger.debug("Set point: \(sp)")
//
logger.debug("Fetching dew point...")
// let dp = try environment.mqttClient.currentDewPoint(
// temperature: tempSensor,
// humidity: humiditySensor,
// units: .imperial
// ).wait()
// logger.info("Dew Point: \(dp.rawValue) \(dp.units.symbol)")
// try environment.mqttClient.publish(
// dewPoint: dp,
// to: environment.topics.sensors.returnAirSensor.dewPoint
// ).wait()
logger.debug("Published dew point...")
Thread.sleep(forTimeInterval: 5) Thread.sleep(forTimeInterval: 5)
} }

View File

@@ -9,7 +9,6 @@ import NIO
import NIOConcurrencyHelpers import NIOConcurrencyHelpers
import XCTest import XCTest
// Can't seem to get tests to work, although we get values when ran from command line.
final class ClientLiveTests: XCTestCase { final class ClientLiveTests: XCTestCase {
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost" static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
let topics = Topics() let topics = Topics()