feat: Updates to newer psychrometrics package. Not yet a working example.

This commit is contained in:
2024-11-09 11:35:30 -05:00
parent e2683d3f06
commit a87addaf0b
13 changed files with 821 additions and 737 deletions

View File

@@ -1,6 +1,15 @@
{ {
"originHash" : "e3e70d8b34d7f35b238e03af18c08ca712051332cf3e429ae1c0ac2823ca2018", "originHash" : "d3104d51323f6bc98cf3ab2930e5a26f72c9a4fdb7640360ba27628672397841",
"pins" : [ "pins" : [
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : {
"revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13",
"version" : "1.0.2"
}
},
{ {
"identity" : "mqtt-nio", "identity" : "mqtt-nio",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -28,6 +37,15 @@
"version" : "1.2.0" "version" : "1.2.0"
} }
}, },
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53",
"version" : "1.0.5"
}
},
{ {
"identity" : "swift-collections", "identity" : "swift-collections",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -37,6 +55,24 @@
"version" : "1.1.4" "version" : "1.1.4"
} }
}, },
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613",
"version" : "1.2.0"
}
},
{
"identity" : "swift-dependencies",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : {
"revision" : "0fc0255e780bf742abeef29dec80924f5f0ae7b9",
"version" : "1.4.1"
}
},
{ {
"identity" : "swift-log", "identity" : "swift-log",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -78,8 +114,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/swift-psychrometrics/swift-psychrometrics", "location" : "https://github.com/swift-psychrometrics/swift-psychrometrics",
"state" : { "state" : {
"revision" : "158b9b12ecd14d36381f5bab8701c4e8eee2d011", "revision" : "6a457f3cefd9477f7aa76b2fb8ad557988c447bd",
"version" : "0.1.0" "version" : "0.2.3"
} }
}, },
{ {
@@ -91,6 +127,15 @@
"version" : "2.6.2" "version" : "2.6.2"
} }
}, },
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
},
{ {
"identity" : "swift-system", "identity" : "swift-system",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -99,6 +144,24 @@
"revision" : "c8a44d836fe7913603e246acab7c528c2e780168", "revision" : "c8a44d836fe7913603e246acab7c528c2e780168",
"version" : "1.4.0" "version" : "1.4.0"
} }
},
{
"identity" : "swift-tagged",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-tagged",
"state" : {
"revision" : "3907a9438f5b57d317001dc99f3f11b46882272b",
"version" : "0.10.0"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb",
"version" : "1.4.2"
}
} }
], ],
"version" : 3 "version" : 3

View File

@@ -25,7 +25,7 @@ let package = Package(
dependencies: [ dependencies: [
.package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.0.0"), .package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-nio", from: "2.0.0"), .package(url: "https://github.com/apple/swift-nio", from: "2.0.0"),
.package(url: "https://github.com/swift-psychrometrics/swift-psychrometrics", exact: "0.1.0"), .package(url: "https://github.com/swift-psychrometrics/swift-psychrometrics", exact: "0.2.3"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0") .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0")
], ],
targets: [ targets: [
@@ -73,7 +73,7 @@ let package = Package(
.target( .target(
name: "Models", name: "Models",
dependencies: [ dependencies: [
.product(name: "Psychrometrics", package: "swift-psychrometrics") .product(name: "PsychrometricClient", package: "swift-psychrometrics")
], ],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
@@ -81,9 +81,9 @@ let package = Package(
name: "Client", name: "Client",
dependencies: [ dependencies: [
"Models", "Models",
.product(name: "CoreUnitTypes", package: "swift-psychrometrics"), // .product(name: "CoreUnitTypes", package: "swift-psychrometrics"),
.product(name: "NIO", package: "swift-nio"), .product(name: "NIO", package: "swift-nio"),
.product(name: "Psychrometrics", package: "swift-psychrometrics") .product(name: "PsychrometricClient", package: "swift-psychrometrics")
], ],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),

View File

@@ -1,44 +1,43 @@
import CoreUnitTypes // import Foundation
import Logging // import Logging
import Foundation // import Models
import Models // import NIO
import NIO // import PsychrometricClient
import Psychrometrics //
// public struct Client {
public struct Client { //
// /// Add the publish listeners to the MQTT Broker, to be notified of published changes.
/// Add the publish listeners to the MQTT Broker, to be notified of published changes. // public var addListeners: () -> Void
public var addListeners: () -> Void //
// /// Connect to the MQTT Broker.
/// Connect to the MQTT Broker. // public var connect: () -> EventLoopFuture<Void>
public var connect: () -> EventLoopFuture<Void> //
// public var publishSensor: (SensorPublishRequest) -> EventLoopFuture<Void>
public var publishSensor: (SensorPublishRequest) -> EventLoopFuture<Void> //
// /// Subscribe to appropriate topics / events.
/// Subscribe to appropriate topics / events. // public var subscribe: () -> EventLoopFuture<Void>
public var subscribe: () -> EventLoopFuture<Void> //
// /// Disconnect and close the connection to the MQTT Broker.
/// Disconnect and close the connection to the MQTT Broker. // public var shutdown: () -> EventLoopFuture<Void>
public var shutdown: () -> EventLoopFuture<Void> //
// public init(
public init( // addListeners: @escaping () -> Void,
addListeners: @escaping () -> Void, // connect: @escaping () -> EventLoopFuture<Void>,
connect: @escaping () -> EventLoopFuture<Void>, // publishSensor: @escaping (SensorPublishRequest) -> EventLoopFuture<Void>,
publishSensor: @escaping (SensorPublishRequest) -> EventLoopFuture<Void>, // shutdown: @escaping () -> EventLoopFuture<Void>,
shutdown: @escaping () -> EventLoopFuture<Void>, // subscribe: @escaping () -> EventLoopFuture<Void>
subscribe: @escaping () -> EventLoopFuture<Void> // ) {
) { // self.addListeners = addListeners
self.addListeners = addListeners // self.connect = connect
self.connect = connect // self.publishSensor = publishSensor
self.publishSensor = publishSensor // self.shutdown = shutdown
self.shutdown = shutdown // self.subscribe = subscribe
self.subscribe = subscribe // }
} //
// public enum SensorPublishRequest {
public enum SensorPublishRequest { // case mixed(State.Sensors.TemperatureHumiditySensor<State.Sensors.MixedAir>)
case mixed(State.Sensors.TemperatureHumiditySensor<State.Sensors.MixedAir>) // case postCoil(State.Sensors.TemperatureHumiditySensor<State.Sensors.PostCoil>)
case postCoil(State.Sensors.TemperatureHumiditySensor<State.Sensors.PostCoil>) // case `return`(State.Sensors.TemperatureHumiditySensor<State.Sensors.Return>)
case `return`(State.Sensors.TemperatureHumiditySensor<State.Sensors.Return>) // case supply(State.Sensors.TemperatureHumiditySensor<State.Sensors.Supply>)
case supply(State.Sensors.TemperatureHumiditySensor<State.Sensors.Supply>) // }
} // }
}

View File

@@ -1,262 +1,260 @@
import CoreUnitTypes // import Logging
import Logging // import Models
import Models // import MQTTNIO
import MQTTNIO // import NIO
import NIO // import NIOFoundationCompat
import NIOFoundationCompat // import PsychrometricClient
import Psychrometrics //
// /// Represents a type that can be initialized by a ``ByteBuffer``.
/// Represents a type that can be initialized by a ``ByteBuffer``. // protocol BufferInitalizable {
protocol BufferInitalizable { // init?(buffer: inout ByteBuffer)
init?(buffer: inout ByteBuffer) // }
} //
// extension Double: BufferInitalizable {
extension Double: BufferInitalizable { //
// /// Attempt to create / parse a double from a byte buffer.
/// Attempt to create / parse a double from a byte buffer. // init?(buffer: inout ByteBuffer) {
init?(buffer: inout ByteBuffer) { // guard let string = buffer.readString(
guard let string = buffer.readString( // length: buffer.readableBytes,
length: buffer.readableBytes, // encoding: String.Encoding.utf8
encoding: String.Encoding.utf8 // )
) // else { return nil }
else { return nil } // self.init(string)
self.init(string) // }
} // }
} //
// extension Temperature: BufferInitalizable {
extension Temperature: BufferInitalizable { // /// Attempt to create / parse a temperature from a byte buffer.
/// Attempt to create / parse a temperature from a byte buffer. // init?(buffer: inout ByteBuffer) {
init?(buffer: inout ByteBuffer) { // guard let value = Double(buffer: &buffer) else { return nil }
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.
/// Attempt to create / parse a relative humidity from a byte buffer. // init?(buffer: inout ByteBuffer) {
init?(buffer: inout ByteBuffer) { // guard let value = Double(buffer: &buffer) else { return nil }
guard let value = Double(buffer: &buffer) else { return nil } // self.init(value)
self.init(value) // }
} // }
} //
// // TODO: Remove below when migrated to async client.
// TODO: Remove below when migrated to async client. // extension MQTTNIO.MQTTClient {
extension MQTTNIO.MQTTClient { // /// Logs a failure for a given topic and error.
/// 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)") // }
} // }
} //
// extension Result where Success == MQTTPublishInfo {
extension Result where Success == MQTTPublishInfo { // func logIfFailure(client: MQTTNIO.MQTTClient, topic: String) -> ByteBuffer? {
func logIfFailure(client: MQTTNIO.MQTTClient, topic: String) -> ByteBuffer? { // switch self {
switch self { // case let .success(value):
case let .success(value): // guard value.topicName == topic else { return nil }
guard value.topicName == topic else { return nil } // return value.payload
return value.payload // case let .failure(error):
case let .failure(error): // client.logFailure(topic: topic, error: error)
client.logFailure(topic: topic, error: error) // return nil
return nil // }
} // }
} // }
} //
// extension Optional where Wrapped == ByteBuffer {
extension Optional where Wrapped == ByteBuffer { //
// func parse<T>(as _: T.Type) -> T? where T: BufferInitalizable {
func parse<T>(as type: T.Type) -> T? where T: BufferInitalizable { // switch self {
switch self { // case var .some(buffer):
case var .some(buffer): // return T(buffer: &buffer)
return T.init(buffer: &buffer) // case .none:
case .none: // return nil
return nil // }
} // }
} // }
} //
// private struct TemperatureAndHumiditySensorKeyPathEnvelope {
fileprivate struct TemperatureAndHumiditySensorKeyPathEnvelope { //
// let humidityTopic: KeyPath<Topics.Sensors, String>
let humidityTopic: KeyPath<Topics.Sensors, String> // let temperatureTopic: KeyPath<Topics.Sensors, String>
let temperatureTopic: KeyPath<Topics.Sensors, String> // let temperatureState: WritableKeyPath<State.Sensors, Temperature?>
let temperatureState: WritableKeyPath<State.Sensors, Temperature?> // let humidityState: WritableKeyPath<State.Sensors, RelativeHumidity?>
let humidityState: WritableKeyPath<State.Sensors, RelativeHumidity?> //
// func addListener(to client: MQTTNIO.MQTTClient, topics: Topics, state: State) {
func addListener(to client: MQTTNIO.MQTTClient, topics: Topics, state: State) { // let temperatureTopic = topics.sensors[keyPath: temperatureTopic]
// client.logger.trace("Adding listener for topic: \(temperatureTopic)")
let temperatureTopic = topics.sensors[keyPath: temperatureTopic] // client.addPublishListener(named: temperatureTopic) { result in
client.logger.trace("Adding listener for topic: \(temperatureTopic)") // result.logIfFailure(client: client, topic: temperatureTopic)
client.addPublishListener(named: temperatureTopic) { result in // .parse(as: Temperature.self)
result.logIfFailure(client: client, topic: temperatureTopic) // .map { temperature in
.parse(as: Temperature.self) // state.sensors[keyPath: temperatureState] = temperature
.map { temperature in // }
state.sensors[keyPath: temperatureState] = temperature // }
} //
} // let humidityTopic = topics.sensors[keyPath: humidityTopic]
// client.logger.trace("Adding listener for topic: \(humidityTopic)")
let humidityTopic = topics.sensors[keyPath: humidityTopic] // client.addPublishListener(named: humidityTopic) { result in
client.logger.trace("Adding listener for topic: \(humidityTopic)") // result.logIfFailure(client: client, topic: humidityTopic)
client.addPublishListener(named: humidityTopic) { result in // .parse(as: RelativeHumidity.self)
result.logIfFailure(client: client, topic: humidityTopic) // .map { humidity in
.parse(as: RelativeHumidity.self) // state.sensors[keyPath: humidityState] = humidity
.map { humidity in // }
state.sensors[keyPath: humidityState] = humidity // }
} // }
} // }
} //
} // extension Array where Element == TemperatureAndHumiditySensorKeyPathEnvelope {
// func addListeners(to client: MQTTNIO.MQTTClient, topics: Topics, state: State) {
extension Array where Element == TemperatureAndHumiditySensorKeyPathEnvelope { // _ = map { envelope in
func addListeners(to client: MQTTNIO.MQTTClient, topics: Topics, state: State) { // envelope.addListener(to: client, topics: topics, state: state)
_ = self.map { envelope in // }
envelope.addListener(to: client, topics: topics, state: state) // }
} // }
} //
} // extension Array where Element == MQTTSubscribeInfo {
// static func sensors(topics: Topics) -> Self {
extension Array where Element == MQTTSubscribeInfo { // [
static func sensors(topics: Topics) -> Self { // .init(topicFilter: topics.sensors.mixedAirSensor.temperature, qos: .atLeastOnce),
[ // .init(topicFilter: topics.sensors.mixedAirSensor.humidity, qos: .atLeastOnce),
.init(topicFilter: topics.sensors.mixedAirSensor.temperature, qos: .atLeastOnce), // .init(topicFilter: topics.sensors.postCoilSensor.temperature, qos: .atLeastOnce),
.init(topicFilter: topics.sensors.mixedAirSensor.humidity, qos: .atLeastOnce), // .init(topicFilter: topics.sensors.postCoilSensor.humidity, qos: .atLeastOnce),
.init(topicFilter: topics.sensors.postCoilSensor.temperature, qos: .atLeastOnce), // .init(topicFilter: topics.sensors.returnAirSensor.temperature, qos: .atLeastOnce),
.init(topicFilter: topics.sensors.postCoilSensor.humidity, qos: .atLeastOnce), // .init(topicFilter: topics.sensors.returnAirSensor.humidity, qos: .atLeastOnce),
.init(topicFilter: topics.sensors.returnAirSensor.temperature, qos: .atLeastOnce), // .init(topicFilter: topics.sensors.supplyAirSensor.temperature, qos: .atLeastOnce),
.init(topicFilter: topics.sensors.returnAirSensor.humidity, qos: .atLeastOnce), // .init(topicFilter: topics.sensors.supplyAirSensor.humidity, qos: .atLeastOnce)
.init(topicFilter: topics.sensors.supplyAirSensor.temperature, qos: .atLeastOnce), // ]
.init(topicFilter: topics.sensors.supplyAirSensor.humidity, qos: .atLeastOnce), // }
] // }
} //
} // extension State {
// func addSensorListeners(to client: MQTTNIO.MQTTClient, topics: Topics) {
extension State { // let envelopes: [TemperatureAndHumiditySensorKeyPathEnvelope] = [
func addSensorListeners(to client: MQTTNIO.MQTTClient, topics: Topics) { // .init(
let envelopes: [TemperatureAndHumiditySensorKeyPathEnvelope] = [ // humidityTopic: \.mixedAirSensor.humidity,
.init( // temperatureTopic: \.mixedAirSensor.temperature,
humidityTopic: \.mixedAirSensor.humidity, // temperatureState: \.mixedAirSensor.temperature,
temperatureTopic: \.mixedAirSensor.temperature, // humidityState: \.mixedAirSensor.humidity
temperatureState: \.mixedAirSensor.temperature, // ),
humidityState: \.mixedAirSensor.humidity // .init(
), // humidityTopic: \.postCoilSensor.humidity,
.init( // temperatureTopic: \.postCoilSensor.temperature,
humidityTopic: \.postCoilSensor.humidity, // temperatureState: \.postCoilSensor.temperature,
temperatureTopic: \.postCoilSensor.temperature, // humidityState: \.postCoilSensor.humidity
temperatureState: \.postCoilSensor.temperature, // ),
humidityState: \.postCoilSensor.humidity // .init(
), // humidityTopic: \.returnAirSensor.humidity,
.init( // temperatureTopic: \.returnAirSensor.temperature,
humidityTopic: \.returnAirSensor.humidity, // temperatureState: \.returnAirSensor.temperature,
temperatureTopic: \.returnAirSensor.temperature, // humidityState: \.returnAirSensor.humidity
temperatureState: \.returnAirSensor.temperature, // ),
humidityState: \.returnAirSensor.humidity // .init(
), // humidityTopic: \.supplyAirSensor.humidity,
.init( // temperatureTopic: \.supplyAirSensor.temperature,
humidityTopic: \.supplyAirSensor.humidity, // temperatureState: \.supplyAirSensor.temperature,
temperatureTopic: \.supplyAirSensor.temperature, // humidityState: \.supplyAirSensor.humidity
temperatureState: \.supplyAirSensor.temperature, // )
humidityState: \.supplyAirSensor.humidity // ]
), // envelopes.addListeners(to: client, topics: topics, state: self)
] // }
envelopes.addListeners(to: client, topics: topics, state: self) // }
} //
} // extension Client.SensorPublishRequest {
//
extension Client.SensorPublishRequest { // func dewPointData(topics: Topics, units: PsychrometricEnvironment.Units?) -> (DewPoint, String)? {
// switch self {
func dewPointData(topics: Topics, units: PsychrometricEnvironment.Units?) -> (DewPoint, String)? { // case let .mixed(sensor):
switch self { // guard let dp = sensor.dewPoint(units: units) else { return nil }
case let .mixed(sensor): // return (dp, topics.sensors.mixedAirSensor.dewPoint)
guard let dp = sensor.dewPoint(units: units) else { return nil } // case let .postCoil(sensor):
return (dp, topics.sensors.mixedAirSensor.dewPoint) // guard let dp = sensor.dewPoint(units: units) else { return nil }
case let .postCoil(sensor): // return (dp, topics.sensors.postCoilSensor.dewPoint)
guard let dp = sensor.dewPoint(units: units) else { return nil } // case let .return(sensor):
return (dp, topics.sensors.postCoilSensor.dewPoint) // guard let dp = sensor.dewPoint(units: units) else { return nil }
case let .return(sensor): // return (dp, topics.sensors.returnAirSensor.dewPoint)
guard let dp = sensor.dewPoint(units: units) else { return nil } // case let .supply(sensor):
return (dp, topics.sensors.returnAirSensor.dewPoint) // guard let dp = sensor.dewPoint(units: units) else { return nil }
case let .supply(sensor): // return (dp, topics.sensors.supplyAirSensor.dewPoint)
guard let dp = sensor.dewPoint(units: units) else { return nil } // }
return (dp, topics.sensors.supplyAirSensor.dewPoint) // }
} //
} // func enthalpyData(altitude: Length, topics: Topics, units: PsychrometricEnvironment.Units?) -> (EnthalpyOf<MoistAir>, String)? {
// switch self {
func enthalpyData(altitude: Length, topics: Topics, units: PsychrometricEnvironment.Units?) -> (EnthalpyOf<MoistAir>, String)? { // case let .mixed(sensor):
switch self { // guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil }
case let .mixed(sensor): // return (enthalpy, topics.sensors.mixedAirSensor.enthalpy)
guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } // case let .postCoil(sensor):
return (enthalpy, topics.sensors.mixedAirSensor.enthalpy) // guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil }
case let .postCoil(sensor): // return (enthalpy, topics.sensors.postCoilSensor.enthalpy)
guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } // case let .return(sensor):
return (enthalpy, topics.sensors.postCoilSensor.enthalpy) // guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil }
case let .return(sensor): // return (enthalpy, topics.sensors.returnAirSensor.enthalpy)
guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } // case let .supply(sensor):
return (enthalpy, topics.sensors.returnAirSensor.enthalpy) // guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil }
case let .supply(sensor): // return (enthalpy, topics.sensors.supplyAirSensor.enthalpy)
guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } // }
return (enthalpy, topics.sensors.supplyAirSensor.enthalpy) // }
} //
} // func setHasProcessed(state: State) {
// switch self {
func setHasProcessed(state: State) { // case .mixed:
switch self { // state.sensors.mixedAirSensor.needsProcessed = false
case .mixed: // case .postCoil:
state.sensors.mixedAirSensor.needsProcessed = false // state.sensors.postCoilSensor.needsProcessed = false
case .postCoil: // case .return:
state.sensors.postCoilSensor.needsProcessed = false // state.sensors.returnAirSensor.needsProcessed = false
case .return: // case .supply:
state.sensors.returnAirSensor.needsProcessed = false // state.sensors.supplyAirSensor.needsProcessed = false
case .supply: // }
state.sensors.supplyAirSensor.needsProcessed = false // }
} // }
} //
} // extension MQTTNIO.MQTTClient {
//
extension MQTTNIO.MQTTClient { // func publishDewPoint(
// request: Client.SensorPublishRequest,
func publishDewPoint( // state: State,
request: Client.SensorPublishRequest, // topics: Topics
state: State, // ) -> EventLoopFuture<(MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics)> {
topics: Topics // guard let (dewPoint, topic) = request.dewPointData(topics: topics, units: state.units)
) -> EventLoopFuture<(MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics)> { // else {
guard let (dewPoint, topic) = request.dewPointData(topics: topics, units: state.units) // logger.trace("No dew point for sensor.")
else { // return eventLoopGroup.next().makeSucceededFuture((self, request, state, topics))
logger.trace("No dew point for sensor.") // }
return eventLoopGroup.next().makeSucceededFuture((self, request, state, topics)) // let roundedDewPoint = round(dewPoint.rawValue * 100) / 100
} // logger.debug("Publishing dew-point: \(dewPoint), to: \(topic)")
let roundedDewPoint = round(dewPoint.rawValue * 100) / 100 // return publish(
logger.debug("Publishing dew-point: \(dewPoint), to: \(topic)") // to: topic,
return publish( // payload: ByteBufferAllocator().buffer(string: "\(roundedDewPoint)"),
to: topic, // qos: .atLeastOnce,
payload: ByteBufferAllocator().buffer(string: "\(roundedDewPoint)"), // retain: true
qos: .atLeastOnce, // )
retain: true // .map { (self, request, state, topics) }
) // }
.map { (self, request, state, topics) } // }
} //
} // extension EventLoopFuture where Value == (Client.SensorPublishRequest, State) {
// func setHasProcessed() -> EventLoopFuture<Void> {
extension EventLoopFuture where Value == (Client.SensorPublishRequest, State) { // map { request, state in
func setHasProcessed() -> EventLoopFuture<Void> { // request.setHasProcessed(state: state)
map { request, state in // }
request.setHasProcessed(state: state) // }
} // }
} //
} // extension EventLoopFuture where Value == (MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics) {
// func publishEnthalpy() -> EventLoopFuture<(Client.SensorPublishRequest, State)> {
extension EventLoopFuture where Value == (MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics) { // flatMap { client, request, state, topics in
func publishEnthalpy() -> EventLoopFuture<(Client.SensorPublishRequest, State)> { // guard let (enthalpy, topic) = request.enthalpyData(altitude: state.altitude, topics: topics, units: state.units)
flatMap { client, request, state, topics in // else {
guard let (enthalpy, topic) = request.enthalpyData(altitude: state.altitude, topics: topics, units: state.units) // client.logger.trace("No enthalpy for sensor.")
else { // return client.eventLoopGroup.next().makeSucceededFuture((request, state))
client.logger.trace("No enthalpy for sensor.") // }
return client.eventLoopGroup.next().makeSucceededFuture((request, state)) // let roundedEnthalpy = round(enthalpy.rawValue * 100) / 100
} // client.logger.debug("Publishing enthalpy: \(enthalpy), to: \(topic)")
let roundedEnthalpy = round(enthalpy.rawValue * 100) / 100 // return client.publish(
client.logger.debug("Publishing enthalpy: \(enthalpy), to: \(topic)") // to: topic,
return client.publish( // payload: ByteBufferAllocator().buffer(string: "\(roundedEnthalpy)"),
to: topic, // qos: .atLeastOnce
payload: ByteBufferAllocator().buffer(string: "\(roundedEnthalpy)"), // )
qos: .atLeastOnce // .map { (request, state) }
) // }
.map { (request, state) } // }
} // }
}
}

View File

@@ -1,41 +1,40 @@
@_exported import Client // @_exported import Client
import CoreUnitTypes // import Foundation
import Foundation // import Models
import Models // import MQTTNIO
import MQTTNIO // import NIO
import NIO // import PsychrometricClient
import Psychrometrics //
// public extension Client {
public extension Client { //
// // The state passed in here needs to be a class or we get escaping errors in the `addListeners` method.
// The state passed in here needs to be a class or we get escaping errors in the `addListeners` method. // static func live(
static func live( // client: MQTTNIO.MQTTClient,
client: MQTTNIO.MQTTClient, // state: State,
state: State, // topics: Topics
topics: Topics // ) -> Self {
) -> Self { // .init(
.init( // addListeners: {
addListeners: { // state.addSensorListeners(to: client, topics: topics)
state.addSensorListeners(to: client, topics: topics) // },
}, // connect: {
connect: { // client.connect()
client.connect() // .map { _ in }
.map { _ in } // },
}, // publishSensor: { request in
publishSensor: { request in // client.publishDewPoint(request: request, state: state, topics: topics)
client.publishDewPoint(request: request, state: state, topics: topics) // .publishEnthalpy()
.publishEnthalpy() // .setHasProcessed()
.setHasProcessed() // },
}, // shutdown: {
shutdown: { // client.disconnect()
client.disconnect() // .map { try? client.syncShutdownGracefully() }
.map { try? client.syncShutdownGracefully() } // },
}, // subscribe: {
subscribe: { // // Sensor subscriptions
// Sensor subscriptions // client.subscribe(to: .sensors(topics: topics))
client.subscribe(to: .sensors(topics: topics)) // .map { _ in }
.map { _ in } // }
} // )
) // }
} // }
}

View File

@@ -1,262 +1,262 @@
import EnvVars // import EnvVars
import Logging // import Logging
import Models // import Models
import MQTTNIO // import MQTTNIO
import NIO // import NIO
import Psychrometrics // import PsychrometricClient
import ServiceLifecycle // import ServiceLifecycle
//
// TODO: Remove. // // TODO: Remove.
// TODO: Pass in eventLoopGroup and MQTTClient. // // TODO: Pass in eventLoopGroup and MQTTClient.
public actor SensorsClient { // public actor SensorsClient {
//
public static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) // public static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
public let client: MQTTClient // public let client: MQTTClient
public private(set) var shuttingDown: Bool // public private(set) var shuttingDown: Bool
public private(set) var sensors: [TemperatureAndHumiditySensor] // public private(set) var sensors: [TemperatureAndHumiditySensor]
//
var logger: Logger { client.logger } // var logger: Logger { client.logger }
//
public init( // public init(
envVars: EnvVars, // envVars: EnvVars,
logger: Logger, // logger: Logger,
sensors: [TemperatureAndHumiditySensor] = [] // sensors: [TemperatureAndHumiditySensor] = []
) { // ) {
let config = MQTTClient.Configuration( // let config = MQTTClient.Configuration(
version: .v3_1_1, // version: .v3_1_1,
userName: envVars.userName, // userName: envVars.userName,
password: envVars.password, // password: envVars.password,
useSSL: false, // useSSL: false,
useWebSockets: false, // useWebSockets: false,
tlsConfiguration: nil, // tlsConfiguration: nil,
webSocketURLPath: nil // webSocketURLPath: nil
) // )
self.client = MQTTClient( // self.client = MQTTClient(
host: envVars.host, // host: envVars.host,
identifier: envVars.identifier, // identifier: envVars.identifier,
eventLoopGroupProvider: .shared(Self.eventLoopGroup), // eventLoopGroupProvider: .shared(Self.eventLoopGroup),
logger: logger, // logger: logger,
configuration: config // configuration: config
) // )
self.shuttingDown = false // self.shuttingDown = false
self.sensors = sensors // self.sensors = sensors
} // }
//
public func addSensor(_ sensor: TemperatureAndHumiditySensor) async throws { // public func addSensor(_ sensor: TemperatureAndHumiditySensor) async throws {
guard sensors.firstIndex(where: { $0.location == sensor.location }) == nil else { // guard sensors.firstIndex(where: { $0.location == sensor.location }) == nil else {
throw SensorExists() // throw SensorExists()
} // }
sensors.append(sensor) // sensors.append(sensor)
} // }
//
public func connect(cleanSession: Bool = true) async { // public func connect(cleanSession: Bool = true) async {
do {
try await client.connect(cleanSession: cleanSession)
client.addCloseListener(named: "SensorsClient") { [self] _ in
guard !self.shuttingDown else { return }
Task {
self.logger.debug("Connection closed.")
self.logger.debug("Reconnecting...")
await self.connect()
}
}
logger.debug("Connection successful.")
} catch {
logger.trace("Connection Failed.\n\(error)")
}
}
public func start() async throws {
await withGracefulShutdownHandler {
await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await self.subscribeToSensors() }
group.addTask { try await self.addSensorListeners() }
}
} onGracefulShutdown: {
Task { await self.shutdown() }
}
// do { // do {
// try await subscribeToSensors() // try await client.connect(cleanSession: cleanSession)
// try await addSensorListeners() // client.addCloseListener(named: "SensorsClient") { [self] _ in
// logger.debug("Begin listening to sensors...") // guard !self.shuttingDown else { return }
// Task {
// self.logger.debug("Connection closed.")
// self.logger.debug("Reconnecting...")
// await self.connect()
// }
// }
// logger.debug("Connection successful.")
// } catch { // } catch {
// logger.trace("Error:\n(error)") // logger.trace("Connection Failed.\(error)")
// }
// }
//
// public func start() async throws {
// await withGracefulShutdownHandler {
// await withThrowingTaskGroup(of: Void.self) { group in
// group.addTask { try await self.subscribeToSensors() }
// group.addTask { try await self.addSensorListeners() }
// }
// } onGracefulShutdown: {
// Task { await self.shutdown() }
// }
// // do {
// // try await subscribeToSensors()
// // try await addSensorListeners()
// // logger.debug("Begin listening to sensors...")
// // } catch {
// // logger.trace("Error:(error)")
// // throw error
// // }
// }
//
// public func shutdown() async {
// shuttingDown = true
// try? await client.disconnect()
// try? await client.shutdown()
// }
//
// /// Subscribe to changes of the temperature and humidity sensors.
// func subscribeToSensors(qos: MQTTQoS = .exactlyOnce) async throws {
// for sensor in sensors {
// try await client.subscribeToSensor(sensor, qos: qos)
// }
// }
//
// private func _addSensorListeners(qos _: MQTTQoS = .exactlyOnce) async throws {
// // try await withThrowingDiscardingTaskGroup { group in
// // group.addTask { try await self.subscribeToSensors(qos: qos) }
//
// for await result in client.createPublishListener() {
// switch result {
// case let .failure(error):
// logger.trace("Error:\(error)")
// case let .success(value):
// let topic = value.topicName
// logger.trace("Received new value for topic: \(topic)")
// if topic.contains("temperature") {
// // do something.
// var buffer = value.payload
// guard let temperature = Temperature(buffer: &buffer) else {
// logger.trace("Decoding error for topic: \(topic)")
// throw DecodingError()
// }
// try sensors.update(topic: topic, keyPath: \.temperature, with: temperature)
// // group.addTask {
// Task {
// try await self.publishUpdates()
// }
//
// } else if topic.contains("humidity") {
// var buffer = value.payload
// // Decode and update the temperature value
// guard let humidity = RelativeHumidity(buffer: &buffer) else {
// logger.debug("Failed to decode humidity from buffer: \(buffer)")
// throw DecodingError()
// }
// try sensors.update(topic: topic, keyPath: \.humidity, with: humidity)
// // group.addTask {
// Task {
// try await self.publishUpdates()
// }
// }
// // }
// }
// }
// }
//
// func addSensorListeners(qos: MQTTQoS = .exactlyOnce) async throws {
// try await subscribeToSensors(qos: qos)
// client.addPublishListener(named: "SensorsClient") { result in
// do {
// switch result {
// case let .success(value):
// var buffer = value.payload
// let topic = value.topicName
// self.logger.trace("Received new value for topic: \(topic)")
//
// if topic.contains("temperature") {
// // Decode and update the temperature value
// guard let temperature = Temperature(buffer: &buffer) else {
// self.logger.debug("Failed to decode temperature from buffer: \(buffer)")
// throw DecodingError()
// }
// try self.sensors.update(topic: topic, keyPath: \.temperature, with: temperature)
// Task { try await self.publishUpdates() }
// } else if topic.contains("humidity") {
// // Decode and update the temperature value
// guard let humidity = RelativeHumidity(buffer: &buffer) else {
// self.logger.debug("Failed to decode humidity from buffer: \(buffer)")
// throw DecodingError()
// }
// try self.sensors.update(topic: topic, keyPath: \.humidity, with: humidity)
// Task { try await self.publishUpdates() }
// }
//
// case let .failure(error):
// self.logger.trace("Error:\(error)")
// throw error // throw error
// } // }
} // } catch {
// self.logger.trace("Error:\(error)")
public func shutdown() async { // }
shuttingDown = true // }
try? await client.disconnect() // }
try? await client.shutdown() //
} // private func publish(double: Double?, to topic: String) async throws {
// guard let double else { return }
/// Subscribe to changes of the temperature and humidity sensors. // let rounded = round(double * 100) / 100
func subscribeToSensors(qos: MQTTQoS = .exactlyOnce) async throws { // logger.debug("Publishing \(rounded), to: \(topic)")
for sensor in sensors { // try await client.publish(
try await client.subscribeToSensor(sensor, qos: qos) // to: topic,
} // payload: ByteBufferAllocator().buffer(string: "\(rounded)"),
} // qos: .exactlyOnce,
// retain: true
private func _addSensorListeners(qos _: MQTTQoS = .exactlyOnce) async throws { // )
// try await withThrowingDiscardingTaskGroup { group in // }
// group.addTask { try await self.subscribeToSensors(qos: qos) } //
// private func publishUpdates() async throws {
for await result in client.createPublishListener() { // for sensor in sensors.filter(\.needsProcessed) {
switch result { // try await publish(double: sensor.dewPoint?.rawValue, to: sensor.topics.dewPoint)
case let .failure(error): // try await publish(double: sensor.enthalpy?.rawValue, to: sensor.topics.enthalpy)
logger.trace("Error:\n\(error)") // try sensors.hasProcessed(sensor)
case let .success(value): // }
let topic = value.topicName // }
logger.trace("Received new value for topic: \(topic)") // }
if topic.contains("temperature") { //
// do something. // // MARK: - Helpers
var buffer = value.payload //
guard let temperature = Temperature(buffer: &buffer) else { // private extension MQTTClient {
logger.trace("Decoding error for topic: \(topic)") //
throw DecodingError() // func subscribeToSensor(
} // _ sensor: TemperatureAndHumiditySensor,
try sensors.update(topic: topic, keyPath: \.temperature, with: temperature) // qos: MQTTQoS = .exactlyOnce
// group.addTask { // ) async throws {
Task { // do {
try await self.publishUpdates() // _ = try await subscribe(to: [
} // MQTTSubscribeInfo(topicFilter: sensor.topics.temperature, qos: qos),
// MQTTSubscribeInfo(topicFilter: sensor.topics.humidity, qos: qos)
} else if topic.contains("humidity") { // ])
var buffer = value.payload // logger.debug("Subscribed to temperature-humidity sensor: \(sensor.id)")
// Decode and update the temperature value // } catch {
guard let humidity = RelativeHumidity(buffer: &buffer) else { // logger.trace("Failed to subscribe to temperature-humidity sensor: \(sensor.id)")
logger.debug("Failed to decode humidity from buffer: \(buffer)") // throw error
throw DecodingError() // }
} // }
try sensors.update(topic: topic, keyPath: \.humidity, with: humidity) // }
// group.addTask { //
Task { // struct DecodingError: Error {}
try await self.publishUpdates() // struct NotFoundError: Error {}
} // struct SensorExists: Error {}
} //
// private extension TemperatureAndHumiditySensor.Topics {
// func contains(_ topic: String) -> Bool {
// temperature == topic || humidity == topic
// }
// }
//
// // TODO: Move to dewpoint-controller/main.swift
// public extension Array where Element == TemperatureAndHumiditySensor {
// static var live: Self {
// TemperatureAndHumiditySensor.Location.allCases.map {
// TemperatureAndHumiditySensor(location: $0)
// }
// }
// }
//
// private extension Array where Element == TemperatureAndHumiditySensor {
//
// mutating func update<V>(
// topic: String,
// keyPath: WritableKeyPath<TemperatureAndHumiditySensor, V>,
// with value: V
// ) throws {
// guard let index = firstIndex(where: { $0.topics.contains(topic) }) else {
// throw NotFoundError()
// }
// self[index][keyPath: keyPath] = value
// }
//
// mutating func hasProcessed(_ sensor: TemperatureAndHumiditySensor) throws {
// guard let index = firstIndex(where: { $0.id == sensor.id }) else {
// throw NotFoundError()
// }
// self[index].needsProcessed = false
// }
//
// } // }
}
}
}
func addSensorListeners(qos: MQTTQoS = .exactlyOnce) async throws {
try await subscribeToSensors(qos: qos)
client.addPublishListener(named: "SensorsClient") { result in
do {
switch result {
case let .success(value):
var buffer = value.payload
let topic = value.topicName
self.logger.trace("Received new value for topic: \(topic)")
if topic.contains("temperature") {
// Decode and update the temperature value
guard let temperature = Temperature(buffer: &buffer) else {
self.logger.debug("Failed to decode temperature from buffer: \(buffer)")
throw DecodingError()
}
try self.sensors.update(topic: topic, keyPath: \.temperature, with: temperature)
Task { try await self.publishUpdates() }
} else if topic.contains("humidity") {
// Decode and update the temperature value
guard let humidity = RelativeHumidity(buffer: &buffer) else {
self.logger.debug("Failed to decode humidity from buffer: \(buffer)")
throw DecodingError()
}
try self.sensors.update(topic: topic, keyPath: \.humidity, with: humidity)
Task { try await self.publishUpdates() }
}
case let .failure(error):
self.logger.trace("Error:\n\(error)")
throw error
}
} catch {
self.logger.trace("Error:\n\(error)")
}
}
}
private func publish(double: Double?, to topic: String) async throws {
guard let double else { return }
let rounded = round(double * 100) / 100
logger.debug("Publishing \(rounded), to: \(topic)")
try await client.publish(
to: topic,
payload: ByteBufferAllocator().buffer(string: "\(rounded)"),
qos: .exactlyOnce,
retain: true
)
}
private func publishUpdates() async throws {
for sensor in sensors.filter(\.needsProcessed) {
try await publish(double: sensor.dewPoint?.rawValue, to: sensor.topics.dewPoint)
try await publish(double: sensor.enthalpy?.rawValue, to: sensor.topics.enthalpy)
try sensors.hasProcessed(sensor)
}
}
}
// MARK: - Helpers
private extension MQTTClient {
func subscribeToSensor(
_ sensor: TemperatureAndHumiditySensor,
qos: MQTTQoS = .exactlyOnce
) async throws {
do {
_ = try await subscribe(to: [
MQTTSubscribeInfo(topicFilter: sensor.topics.temperature, qos: qos),
MQTTSubscribeInfo(topicFilter: sensor.topics.humidity, qos: qos)
])
logger.debug("Subscribed to temperature-humidity sensor: \(sensor.id)")
} catch {
logger.trace("Failed to subscribe to temperature-humidity sensor: \(sensor.id)")
throw error
}
}
}
struct DecodingError: Error {}
struct NotFoundError: Error {}
struct SensorExists: Error {}
private extension TemperatureAndHumiditySensor.Topics {
func contains(_ topic: String) -> Bool {
temperature == topic || humidity == topic
}
}
// TODO: Move to dewpoint-controller/main.swift
public extension Array where Element == TemperatureAndHumiditySensor {
static var live: Self {
TemperatureAndHumiditySensor.Location.allCases.map {
TemperatureAndHumiditySensor(location: $0)
}
}
}
private extension Array where Element == TemperatureAndHumiditySensor {
mutating func update<V>(
topic: String,
keyPath: WritableKeyPath<TemperatureAndHumiditySensor, V>,
with value: V
) throws {
guard let index = firstIndex(where: { $0.topics.contains(topic) }) else {
throw NotFoundError()
}
self[index][keyPath: keyPath] = value
}
mutating func hasProcessed(_ sensor: TemperatureAndHumiditySensor) throws {
guard let index = firstIndex(where: { $0.id == sensor.id }) else {
throw NotFoundError()
}
self[index].needsProcessed = false
}
}

View File

@@ -1,4 +1,4 @@
import CoreUnitTypes import PsychrometricClient
// TODO: Remove // TODO: Remove
@@ -21,7 +21,7 @@ public enum Mode: Equatable {
public enum HumidifyMode: Equatable { public enum HumidifyMode: Equatable {
/// Control humidifying based off dew-point. /// Control humidifying based off dew-point.
case dewPoint(Temperature) case dewPoint(DewPoint)
/// Control humidifying based off relative humidity. /// Control humidifying based off relative humidity.
case relativeHumidity(RelativeHumidity) case relativeHumidity(RelativeHumidity)
@@ -31,7 +31,7 @@ public enum Mode: Equatable {
public enum DehumidifyMode: Equatable { public enum DehumidifyMode: Equatable {
/// Control dehumidifying based off dew-point. /// Control dehumidifying based off dew-point.
case dewPoint(high: Temperature, low: Temperature) case dewPoint(high: DewPoint, low: DewPoint)
/// Control humidifying based off relative humidity. /// Control humidifying based off relative humidity.
case relativeHumidity(high: RelativeHumidity, low: RelativeHumidity) case relativeHumidity(high: RelativeHumidity, low: RelativeHumidity)

View File

@@ -1,5 +1,5 @@
import Foundation import Foundation
@preconcurrency import Psychrometrics import PsychrometricClient
// TODO: Remove // TODO: Remove
// TODO: Make this a struct, then create a Store class that holds the state?? // TODO: Make this a struct, then create a Store class that holds the state??
@@ -7,16 +7,12 @@ public final class State {
public var altitude: Length public var altitude: Length
public var sensors: Sensors public var sensors: Sensors
public var units: PsychrometricEnvironment.Units { public var units: PsychrometricUnits
didSet {
PsychrometricEnvironment.shared.units = units
}
}
public init( public init(
altitude: Length = .seaLevel, altitude: Length = .seaLevel,
sensors: Sensors = .init(), sensors: Sensors = .init(),
units: PsychrometricEnvironment.Units = .imperial units: PsychrometricUnits = .imperial
) { ) {
self.altitude = altitude self.altitude = altitude
self.sensors = sensors self.sensors = sensors
@@ -56,7 +52,7 @@ public extension State.Sensors {
struct TemperatureHumiditySensor<Location>: Equatable { struct TemperatureHumiditySensor<Location>: Equatable {
@TrackedChanges @TrackedChanges
public var temperature: Temperature? public var temperature: DryBulb?
@TrackedChanges @TrackedChanges
public var humidity: RelativeHumidity? public var humidity: RelativeHumidity?
@@ -69,26 +65,26 @@ public extension State.Sensors {
} }
} }
public func dewPoint(units: PsychrometricEnvironment.Units? = nil) -> DewPoint? { // WARN: Fix me.
public func dewPoint(units _: PsychrometricUnits? = 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 nil
// return .init(dryBulb: temperature, humidity: humidity, units: units)
} }
public func enthalpy(altitude: Length, units: PsychrometricEnvironment.Units? = nil) -> EnthalpyOf<MoistAir>? { // WARN: Fix me.
public func enthalpy(altitude _: Length, units _: PsychrometricUnits? = 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 nil
// return .init(dryBulb: temperature, humidity: humidity, altitude: altitude, units: units)
} }
public init( public init(
temperature: Temperature? = nil, temperature: DryBulb? = nil,
humidity: RelativeHumidity? = nil, humidity: RelativeHumidity? = nil,
needsProcessed: Bool = false needsProcessed: Bool = false
) { ) {

View File

@@ -1,10 +1,13 @@
@preconcurrency import Psychrometrics import Dependencies
import PsychrometricClient
/// Represents a temperature and humidity sensor that can be used to derive /// Represents a temperature and humidity sensor that can be used to derive
/// the dew-point temperature and enthalpy values. /// the dew-point temperature and enthalpy values.
/// ///
/// > Note: Temperature values are received in `celsius`. /// > Note: Temperature values are received in `celsius`.
public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @unchecked Sendable { public struct TemperatureAndHumiditySensor: Identifiable, Sendable {
@Dependency(\.psychrometricClient) private var psychrometrics
/// The identifier of the sensor, same as the location. /// The identifier of the sensor, same as the location.
public var id: Location { location } public var id: Location { location }
@@ -21,7 +24,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @
/// The current temperature value of the sensor. /// The current temperature value of the sensor.
@TrackedChanges @TrackedChanges
public var temperature: Temperature? public var temperature: DryBulb?
/// The topics to listen for updated sensor values. /// The topics to listen for updated sensor values.
public let topics: Topics public let topics: Topics
@@ -37,7 +40,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @
public init( public init(
location: Location, location: Location,
altitude: Length = .feet(800.0), altitude: Length = .feet(800.0),
temperature: Temperature? = nil, temperature: DryBulb? = nil,
humidity: RelativeHumidity? = nil, humidity: RelativeHumidity? = nil,
needsProcessed: Bool = false, needsProcessed: Bool = false,
topics: Topics? = nil topics: Topics? = nil
@@ -51,22 +54,26 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @
/// The calculated dew-point temperature of the sensor. /// The calculated dew-point temperature of the sensor.
public var dewPoint: DewPoint? { public var dewPoint: DewPoint? {
get async {
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) return try? await psychrometrics.dewPoint(.dryBulb(temperature, relativeHumidity: humidity))
// return .init(dryBulb: temperature, humidity: humidity)
}
} }
/// The calculated enthalpy of the sensor. /// The calculated enthalpy of the sensor.
public var enthalpy: EnthalpyOf<MoistAir>? { public var enthalpy: EnthalpyOf<MoistAir>? {
get async {
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) return try? await psychrometrics.enthalpy.moistAir(
.dryBulb(temperature, relativeHumidity: humidity, altitude: altitude)
)
// return .init(dryBulb: temperature, humidity: humidity, altitude: altitude)
}
} }
/// Check whether any of the sensor values have changed and need processed. /// Check whether any of the sensor values have changed and need processed.
@@ -82,7 +89,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @
/// Represents the different locations of a temperature and humidity sensor, which can /// Represents the different locations of a temperature and humidity sensor, which can
/// be used to derive the topic to both listen and publish new values to. /// be used to derive the topic to both listen and publish new values to.
public enum Location: String, CaseIterable, Equatable, Hashable { public enum Location: String, CaseIterable, Equatable, Hashable, Sendable {
case mixedAir = "mixed_air" case mixedAir = "mixed_air"
case postCoil = "post_coil" case postCoil = "post_coil"
case `return` case `return`
@@ -90,7 +97,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @
} }
/// Represents the MQTT topics to listen for updated sensor values on. /// Represents the MQTT topics to listen for updated sensor values on.
public struct Topics: Equatable, Hashable { public struct Topics: Equatable, Hashable, Sendable {
/// The dew-point temperature topic for the sensor. /// The dew-point temperature topic for the sensor.
public let dewPoint: String public let dewPoint: String

View File

@@ -96,3 +96,5 @@ extension TrackedChanges: Hashable where Value: Hashable {
hasher.combine(needsProcessed) hasher.combine(needsProcessed)
} }
} }
extension TrackedChanges: Sendable where Value: Sendable {}

View File

@@ -1,10 +1,9 @@
import CoreUnitTypes
import Logging import Logging
import Models import Models
import MQTTNIO import MQTTNIO
import NIO import NIO
import NIOFoundationCompat import NIOFoundationCompat
import Psychrometrics import SharedModels
/// Represents a type that can be initialized by a ``ByteBuffer``. /// Represents a type that can be initialized by a ``ByteBuffer``.
protocol BufferInitalizable { protocol BufferInitalizable {
@@ -24,18 +23,39 @@ extension Double: BufferInitalizable {
} }
} }
extension Temperature: BufferInitalizable { // extension DryBulb: BufferInitalizable {
/// Attempt to create / parse a temperature from a byte buffer. // /// 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(.init(value, units: .celsius))
// }
// }
extension Tagged: BufferInitalizable where RawValue: BufferInitalizable {
init?(buffer: inout ByteBuffer) { init?(buffer: inout ByteBuffer) {
guard let value = Double(buffer: &buffer) else { return nil } guard let value = RawValue(buffer: &buffer) else { return nil }
self.init(value, units: .celsius) self.init(value)
} }
} }
extension RelativeHumidity: BufferInitalizable { extension Humidity<Relative>: BufferInitalizable {
/// Attempt to create / parse a relative humidity from a byte buffer.
init?(buffer: inout ByteBuffer) { init?(buffer: inout ByteBuffer) {
guard let value = Double(buffer: &buffer) else { return nil } guard let value = Double(buffer: &buffer) else { return nil }
self.init(value) self.init(value)
} }
} }
extension Temperature<DryAir>: BufferInitalizable {
init?(buffer: inout ByteBuffer) {
guard let value = Double(buffer: &buffer) else { return nil }
self.init(value)
}
}
// extension RelativeHumidity: BufferInitalizable {
// /// Attempt to create / parse a relative humidity from a byte buffer.
// init?(buffer: inout ByteBuffer) {
// guard let value = Double(buffer: &buffer) else { return nil }
// self.init(value)
// }
// }

View File

@@ -3,7 +3,7 @@ import Logging
import Models import Models
@preconcurrency import MQTTNIO @preconcurrency import MQTTNIO
import NIO import NIO
import Psychrometrics import PsychrometricClient
import ServiceLifecycle import ServiceLifecycle
public actor SensorsService: Service { public actor SensorsService: Service {
@@ -52,7 +52,7 @@ public actor SensorsService: Service {
if topic.contains("temperature") { if topic.contains("temperature") {
// do something. // do something.
var buffer = value.payload var buffer = value.payload
guard let temperature = Temperature(buffer: &buffer) else { guard let temperature = DryBulb(buffer: &buffer) else {
logger.trace("Decoding error for topic: \(topic)") logger.trace("Decoding error for topic: \(topic)")
throw DecodingError() throw DecodingError()
} }
@@ -96,8 +96,8 @@ public actor SensorsService: Service {
private func publishUpdates() async throws { private func publishUpdates() async throws {
for sensor in sensors.filter(\.needsProcessed) { for sensor in sensors.filter(\.needsProcessed) {
try await publish(double: sensor.dewPoint?.rawValue, to: sensor.topics.dewPoint) try await publish(double: sensor.dewPoint?.value, to: sensor.topics.dewPoint)
try await publish(double: sensor.enthalpy?.rawValue, to: sensor.topics.enthalpy) try await publish(double: sensor.enthalpy?.value, to: sensor.topics.enthalpy)
try sensors.hasProcessed(sensor) try sensors.hasProcessed(sensor)
} }
} }

View File

@@ -1,74 +1,74 @@
import Bootstrap // import Bootstrap
import ClientLive // 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 TopicsLive
import Foundation // import Foundation
var logger: Logger = {
var logger = Logger(label: "dewPoint-logger")
logger.logLevel = .debug
return logger
}()
logger.info("Starting Swift Dew Point Controller!")
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
var environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger, autoConnect: false).wait()
// Set the log level to info only in production mode.
if environment.envVars.appEnv == .production {
logger.debug("Updating logging level to info.")
logger.logLevel = .info
}
// Set up the client, topics and state.
environment.topics = .live
let state = State()
let client = Client.live(client: environment.mqttClient, state: state, topics: environment.topics)
defer {
logger.debug("Disconnecting")
}
// Add topic listeners.
client.addListeners()
while true {
if !environment.mqttClient.isActive() {
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("Published dew point...") // var logger: Logger = {
// var logger = Logger(label: "dewPoint-logger")
Thread.sleep(forTimeInterval: 5) // logger.logLevel = .debug
} // return logger
// }()
//
// logger.info("Starting Swift Dew Point Controller!")
//
// let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
// var environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger, autoConnect: false).wait()
//
// // Set the log level to info only in production mode.
// if environment.envVars.appEnv == .production {
// logger.debug("Updating logging level to info.")
// logger.logLevel = .info
// }
//
// // Set up the client, topics and state.
// environment.topics = .live
// let state = State()
// let client = Client.live(client: environment.mqttClient, state: state, topics: environment.topics)
//
// defer {
// logger.debug("Disconnecting")
// }
//
// // Add topic listeners.
// client.addListeners()
//
// while true {
// if !environment.mqttClient.isActive() {
// 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("Published dew point...")
//
// Thread.sleep(forTimeInterval: 5)
// }