From a87addaf0b2435552214482b1fb01332b36e2205 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Sat, 9 Nov 2024 11:35:30 -0500 Subject: [PATCH] feat: Updates to newer psychrometrics package. Not yet a working example. --- Package.resolved | 69 ++- Package.swift | 8 +- Sources/Client/Interface.swift | 87 ++- Sources/ClientLive/Helpers.swift | 522 +++++++++--------- Sources/ClientLive/Live.swift | 81 ++- Sources/ClientLive/SensorsClient.swift | 516 ++++++++--------- Sources/Models/Mode.swift | 6 +- Sources/Models/State.swift | 34 +- .../Models/TemperatureAndHumiditySensor.swift | 43 +- Sources/Models/TrackedChanges.swift | 2 + Sources/SensorsService/Helpers.swift | 36 +- Sources/SensorsService/SensorsService.swift | 8 +- Sources/dewPoint-controller/main.swift | 146 ++--- 13 files changed, 821 insertions(+), 737 deletions(-) diff --git a/Package.resolved b/Package.resolved index a5af3f9..379a782 100755 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "e3e70d8b34d7f35b238e03af18c08ca712051332cf3e429ae1c0ac2823ca2018", + "originHash" : "d3104d51323f6bc98cf3ab2930e5a26f72c9a4fdb7640360ba27628672397841", "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" + } + }, { "identity" : "mqtt-nio", "kind" : "remoteSourceControl", @@ -28,6 +37,15 @@ "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", "kind" : "remoteSourceControl", @@ -37,6 +55,24 @@ "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", "kind" : "remoteSourceControl", @@ -78,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-psychrometrics/swift-psychrometrics", "state" : { - "revision" : "158b9b12ecd14d36381f5bab8701c4e8eee2d011", - "version" : "0.1.0" + "revision" : "6a457f3cefd9477f7aa76b2fb8ad557988c447bd", + "version" : "0.2.3" } }, { @@ -91,6 +127,15 @@ "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", "kind" : "remoteSourceControl", @@ -99,6 +144,24 @@ "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", "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 diff --git a/Package.swift b/Package.swift index f28f3ee..71af949 100755 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( dependencies: [ .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/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") ], targets: [ @@ -73,7 +73,7 @@ let package = Package( .target( name: "Models", dependencies: [ - .product(name: "Psychrometrics", package: "swift-psychrometrics") + .product(name: "PsychrometricClient", package: "swift-psychrometrics") ], swiftSettings: swiftSettings ), @@ -81,9 +81,9 @@ let package = Package( name: "Client", dependencies: [ "Models", - .product(name: "CoreUnitTypes", package: "swift-psychrometrics"), + // .product(name: "CoreUnitTypes", package: "swift-psychrometrics"), .product(name: "NIO", package: "swift-nio"), - .product(name: "Psychrometrics", package: "swift-psychrometrics") + .product(name: "PsychrometricClient", package: "swift-psychrometrics") ], swiftSettings: swiftSettings ), diff --git a/Sources/Client/Interface.swift b/Sources/Client/Interface.swift index 8bdfa76..271085d 100755 --- a/Sources/Client/Interface.swift +++ b/Sources/Client/Interface.swift @@ -1,44 +1,43 @@ -import CoreUnitTypes -import Logging -import Foundation -import Models -import NIO -import Psychrometrics - -public struct Client { - - /// Add the publish listeners to the MQTT Broker, to be notified of published changes. - public var addListeners: () -> Void - - /// Connect to the MQTT Broker. - public var connect: () -> EventLoopFuture - - public var publishSensor: (SensorPublishRequest) -> EventLoopFuture - - /// Subscribe to appropriate topics / events. - public var subscribe: () -> EventLoopFuture - - /// Disconnect and close the connection to the MQTT Broker. - public var shutdown: () -> EventLoopFuture - - public init( - addListeners: @escaping () -> Void, - connect: @escaping () -> EventLoopFuture, - publishSensor: @escaping (SensorPublishRequest) -> EventLoopFuture, - shutdown: @escaping () -> EventLoopFuture, - subscribe: @escaping () -> EventLoopFuture - ) { - self.addListeners = addListeners - self.connect = connect - self.publishSensor = publishSensor - self.shutdown = shutdown - self.subscribe = subscribe - } - - public enum SensorPublishRequest { - case mixed(State.Sensors.TemperatureHumiditySensor) - case postCoil(State.Sensors.TemperatureHumiditySensor) - case `return`(State.Sensors.TemperatureHumiditySensor) - case supply(State.Sensors.TemperatureHumiditySensor) - } -} +// import Foundation +// import Logging +// import Models +// import NIO +// import PsychrometricClient +// +// public struct Client { +// +// /// Add the publish listeners to the MQTT Broker, to be notified of published changes. +// public var addListeners: () -> Void +// +// /// Connect to the MQTT Broker. +// public var connect: () -> EventLoopFuture +// +// public var publishSensor: (SensorPublishRequest) -> EventLoopFuture +// +// /// Subscribe to appropriate topics / events. +// public var subscribe: () -> EventLoopFuture +// +// /// Disconnect and close the connection to the MQTT Broker. +// public var shutdown: () -> EventLoopFuture +// +// public init( +// addListeners: @escaping () -> Void, +// connect: @escaping () -> EventLoopFuture, +// publishSensor: @escaping (SensorPublishRequest) -> EventLoopFuture, +// shutdown: @escaping () -> EventLoopFuture, +// subscribe: @escaping () -> EventLoopFuture +// ) { +// self.addListeners = addListeners +// self.connect = connect +// self.publishSensor = publishSensor +// self.shutdown = shutdown +// self.subscribe = subscribe +// } +// +// public enum SensorPublishRequest { +// case mixed(State.Sensors.TemperatureHumiditySensor) +// case postCoil(State.Sensors.TemperatureHumiditySensor) +// case `return`(State.Sensors.TemperatureHumiditySensor) +// case supply(State.Sensors.TemperatureHumiditySensor) +// } +// } diff --git a/Sources/ClientLive/Helpers.swift b/Sources/ClientLive/Helpers.swift index 4998ca5..435dc51 100755 --- a/Sources/ClientLive/Helpers.swift +++ b/Sources/ClientLive/Helpers.swift @@ -1,262 +1,260 @@ -import CoreUnitTypes -import Logging -import Models -import MQTTNIO -import NIO -import NIOFoundationCompat -import Psychrometrics - -/// Represents a type that can be initialized by a ``ByteBuffer``. -protocol BufferInitalizable { - init?(buffer: inout ByteBuffer) -} - -extension Double: BufferInitalizable { - - /// Attempt to create / parse a double from a byte buffer. - init?(buffer: inout ByteBuffer) { - guard let string = buffer.readString( - length: buffer.readableBytes, - encoding: String.Encoding.utf8 - ) - else { return nil } - self.init(string) - } -} - -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) - } -} - -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) - } -} - -// TODO: Remove below when migrated to async client. -extension MQTTNIO.MQTTClient { - /// Logs a failure for a given topic and error. - func logFailure(topic: String, error: Error) { - logger.error("\(topic): \(error)") - } -} - -extension Result where Success == MQTTPublishInfo { - func logIfFailure(client: MQTTNIO.MQTTClient, topic: String) -> ByteBuffer? { - switch self { - case let .success(value): - guard value.topicName == topic else { return nil } - return value.payload - case let .failure(error): - client.logFailure(topic: topic, error: error) - return nil - } - } -} - -extension Optional where Wrapped == ByteBuffer { - - func parse(as type: T.Type) -> T? where T: BufferInitalizable { - switch self { - case var .some(buffer): - return T.init(buffer: &buffer) - case .none: - return nil - } - } -} - -fileprivate struct TemperatureAndHumiditySensorKeyPathEnvelope { - - let humidityTopic: KeyPath - let temperatureTopic: KeyPath - let temperatureState: WritableKeyPath - let humidityState: WritableKeyPath - - func addListener(to client: MQTTNIO.MQTTClient, topics: Topics, state: State) { - - let temperatureTopic = topics.sensors[keyPath: temperatureTopic] - client.logger.trace("Adding listener for topic: \(temperatureTopic)") - client.addPublishListener(named: temperatureTopic) { result in - result.logIfFailure(client: client, topic: temperatureTopic) - .parse(as: Temperature.self) - .map { temperature in - state.sensors[keyPath: temperatureState] = temperature - } - } - - let humidityTopic = topics.sensors[keyPath: humidityTopic] - client.logger.trace("Adding listener for topic: \(humidityTopic)") - client.addPublishListener(named: humidityTopic) { result in - result.logIfFailure(client: client, topic: humidityTopic) - .parse(as: RelativeHumidity.self) - .map { humidity in - state.sensors[keyPath: humidityState] = humidity - } - } - } -} - -extension Array where Element == TemperatureAndHumiditySensorKeyPathEnvelope { - func addListeners(to client: MQTTNIO.MQTTClient, 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 { - [ - .init(topicFilter: topics.sensors.mixedAirSensor.temperature, qos: .atLeastOnce), - .init(topicFilter: topics.sensors.mixedAirSensor.humidity, qos: .atLeastOnce), - .init(topicFilter: topics.sensors.postCoilSensor.temperature, qos: .atLeastOnce), - .init(topicFilter: topics.sensors.postCoilSensor.humidity, qos: .atLeastOnce), - .init(topicFilter: topics.sensors.returnAirSensor.temperature, qos: .atLeastOnce), - .init(topicFilter: topics.sensors.returnAirSensor.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) { - let envelopes: [TemperatureAndHumiditySensorKeyPathEnvelope] = [ - .init( - humidityTopic: \.mixedAirSensor.humidity, - temperatureTopic: \.mixedAirSensor.temperature, - temperatureState: \.mixedAirSensor.temperature, - humidityState: \.mixedAirSensor.humidity - ), - .init( - humidityTopic: \.postCoilSensor.humidity, - temperatureTopic: \.postCoilSensor.temperature, - temperatureState: \.postCoilSensor.temperature, - humidityState: \.postCoilSensor.humidity - ), - .init( - humidityTopic: \.returnAirSensor.humidity, - temperatureTopic: \.returnAirSensor.temperature, - temperatureState: \.returnAirSensor.temperature, - humidityState: \.returnAirSensor.humidity - ), - .init( - humidityTopic: \.supplyAirSensor.humidity, - temperatureTopic: \.supplyAirSensor.temperature, - temperatureState: \.supplyAirSensor.temperature, - humidityState: \.supplyAirSensor.humidity - ), - ] - envelopes.addListeners(to: client, topics: topics, state: self) - } -} - -extension Client.SensorPublishRequest { - - func dewPointData(topics: Topics, units: PsychrometricEnvironment.Units?) -> (DewPoint, String)? { - switch self { - case let .mixed(sensor): - guard let dp = sensor.dewPoint(units: units) else { return nil } - return (dp, topics.sensors.mixedAirSensor.dewPoint) - case let .postCoil(sensor): - guard let dp = sensor.dewPoint(units: units) else { return nil } - return (dp, topics.sensors.postCoilSensor.dewPoint) - case let .return(sensor): - guard let dp = sensor.dewPoint(units: units) else { return nil } - return (dp, topics.sensors.returnAirSensor.dewPoint) - case let .supply(sensor): - 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, String)? { - switch self { - case let .mixed(sensor): - guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } - return (enthalpy, topics.sensors.mixedAirSensor.enthalpy) - case let .postCoil(sensor): - guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } - return (enthalpy, topics.sensors.postCoilSensor.enthalpy) - case let .return(sensor): - guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } - return (enthalpy, topics.sensors.returnAirSensor.enthalpy) - case let .supply(sensor): - guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } - return (enthalpy, topics.sensors.supplyAirSensor.enthalpy) - } - } - - func setHasProcessed(state: State) { - switch self { - case .mixed: - state.sensors.mixedAirSensor.needsProcessed = false - case .postCoil: - state.sensors.postCoilSensor.needsProcessed = false - case .return: - state.sensors.returnAirSensor.needsProcessed = false - case .supply: - state.sensors.supplyAirSensor.needsProcessed = false - } - } -} - -extension MQTTNIO.MQTTClient { - - func publishDewPoint( - request: Client.SensorPublishRequest, - state: State, - topics: Topics - ) -> EventLoopFuture<(MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics)> { - guard let (dewPoint, topic) = request.dewPointData(topics: topics, units: state.units) - else { - 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)") - return publish( - to: topic, - payload: ByteBufferAllocator().buffer(string: "\(roundedDewPoint)"), - qos: .atLeastOnce, - retain: true - ) - .map { (self, request, state, topics) } - } -} - -extension EventLoopFuture where Value == (Client.SensorPublishRequest, State) { - func setHasProcessed() -> EventLoopFuture { - map { request, state in - request.setHasProcessed(state: state) - } - } -} - -extension EventLoopFuture where Value == (MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics) { - func publishEnthalpy() -> EventLoopFuture<(Client.SensorPublishRequest, State)> { - flatMap { client, request, state, topics in - guard let (enthalpy, topic) = request.enthalpyData(altitude: state.altitude, topics: topics, units: state.units) - else { - 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)") - return client.publish( - to: topic, - payload: ByteBufferAllocator().buffer(string: "\(roundedEnthalpy)"), - qos: .atLeastOnce - ) - .map { (request, state) } - } - } -} +// import Logging +// import Models +// import MQTTNIO +// import NIO +// import NIOFoundationCompat +// import PsychrometricClient +// +// /// Represents a type that can be initialized by a ``ByteBuffer``. +// protocol BufferInitalizable { +// init?(buffer: inout ByteBuffer) +// } +// +// extension Double: BufferInitalizable { +// +// /// Attempt to create / parse a double from a byte buffer. +// init?(buffer: inout ByteBuffer) { +// guard let string = buffer.readString( +// length: buffer.readableBytes, +// encoding: String.Encoding.utf8 +// ) +// else { return nil } +// self.init(string) +// } +// } +// +// 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) +// } +// } +// +// 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) +// } +// } +// +// // TODO: Remove below when migrated to async client. +// extension MQTTNIO.MQTTClient { +// /// Logs a failure for a given topic and error. +// func logFailure(topic: String, error: Error) { +// logger.error("\(topic): \(error)") +// } +// } +// +// extension Result where Success == MQTTPublishInfo { +// func logIfFailure(client: MQTTNIO.MQTTClient, topic: String) -> ByteBuffer? { +// switch self { +// case let .success(value): +// guard value.topicName == topic else { return nil } +// return value.payload +// case let .failure(error): +// client.logFailure(topic: topic, error: error) +// return nil +// } +// } +// } +// +// extension Optional where Wrapped == ByteBuffer { +// +// func parse(as _: T.Type) -> T? where T: BufferInitalizable { +// switch self { +// case var .some(buffer): +// return T(buffer: &buffer) +// case .none: +// return nil +// } +// } +// } +// +// private struct TemperatureAndHumiditySensorKeyPathEnvelope { +// +// let humidityTopic: KeyPath +// let temperatureTopic: KeyPath +// let temperatureState: WritableKeyPath +// let humidityState: WritableKeyPath +// +// func addListener(to client: MQTTNIO.MQTTClient, topics: Topics, state: State) { +// let temperatureTopic = topics.sensors[keyPath: temperatureTopic] +// client.logger.trace("Adding listener for topic: \(temperatureTopic)") +// client.addPublishListener(named: temperatureTopic) { result in +// result.logIfFailure(client: client, topic: temperatureTopic) +// .parse(as: Temperature.self) +// .map { temperature in +// state.sensors[keyPath: temperatureState] = temperature +// } +// } +// +// let humidityTopic = topics.sensors[keyPath: humidityTopic] +// client.logger.trace("Adding listener for topic: \(humidityTopic)") +// client.addPublishListener(named: humidityTopic) { result in +// result.logIfFailure(client: client, topic: humidityTopic) +// .parse(as: RelativeHumidity.self) +// .map { humidity in +// state.sensors[keyPath: humidityState] = humidity +// } +// } +// } +// } +// +// extension Array where Element == TemperatureAndHumiditySensorKeyPathEnvelope { +// func addListeners(to client: MQTTNIO.MQTTClient, topics: Topics, state: State) { +// _ = map { envelope in +// envelope.addListener(to: client, topics: topics, state: state) +// } +// } +// } +// +// 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.postCoilSensor.temperature, qos: .atLeastOnce), +// .init(topicFilter: topics.sensors.postCoilSensor.humidity, qos: .atLeastOnce), +// .init(topicFilter: topics.sensors.returnAirSensor.temperature, qos: .atLeastOnce), +// .init(topicFilter: topics.sensors.returnAirSensor.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) { +// let envelopes: [TemperatureAndHumiditySensorKeyPathEnvelope] = [ +// .init( +// humidityTopic: \.mixedAirSensor.humidity, +// temperatureTopic: \.mixedAirSensor.temperature, +// temperatureState: \.mixedAirSensor.temperature, +// humidityState: \.mixedAirSensor.humidity +// ), +// .init( +// humidityTopic: \.postCoilSensor.humidity, +// temperatureTopic: \.postCoilSensor.temperature, +// temperatureState: \.postCoilSensor.temperature, +// humidityState: \.postCoilSensor.humidity +// ), +// .init( +// humidityTopic: \.returnAirSensor.humidity, +// temperatureTopic: \.returnAirSensor.temperature, +// temperatureState: \.returnAirSensor.temperature, +// humidityState: \.returnAirSensor.humidity +// ), +// .init( +// humidityTopic: \.supplyAirSensor.humidity, +// temperatureTopic: \.supplyAirSensor.temperature, +// temperatureState: \.supplyAirSensor.temperature, +// humidityState: \.supplyAirSensor.humidity +// ) +// ] +// envelopes.addListeners(to: client, topics: topics, state: self) +// } +// } +// +// extension Client.SensorPublishRequest { +// +// func dewPointData(topics: Topics, units: PsychrometricEnvironment.Units?) -> (DewPoint, String)? { +// switch self { +// case let .mixed(sensor): +// guard let dp = sensor.dewPoint(units: units) else { return nil } +// return (dp, topics.sensors.mixedAirSensor.dewPoint) +// case let .postCoil(sensor): +// guard let dp = sensor.dewPoint(units: units) else { return nil } +// return (dp, topics.sensors.postCoilSensor.dewPoint) +// case let .return(sensor): +// guard let dp = sensor.dewPoint(units: units) else { return nil } +// return (dp, topics.sensors.returnAirSensor.dewPoint) +// case let .supply(sensor): +// 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, String)? { +// switch self { +// case let .mixed(sensor): +// guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } +// return (enthalpy, topics.sensors.mixedAirSensor.enthalpy) +// case let .postCoil(sensor): +// guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } +// return (enthalpy, topics.sensors.postCoilSensor.enthalpy) +// case let .return(sensor): +// guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } +// return (enthalpy, topics.sensors.returnAirSensor.enthalpy) +// case let .supply(sensor): +// guard let enthalpy = sensor.enthalpy(altitude: altitude, units: units) else { return nil } +// return (enthalpy, topics.sensors.supplyAirSensor.enthalpy) +// } +// } +// +// func setHasProcessed(state: State) { +// switch self { +// case .mixed: +// state.sensors.mixedAirSensor.needsProcessed = false +// case .postCoil: +// state.sensors.postCoilSensor.needsProcessed = false +// case .return: +// state.sensors.returnAirSensor.needsProcessed = false +// case .supply: +// state.sensors.supplyAirSensor.needsProcessed = false +// } +// } +// } +// +// extension MQTTNIO.MQTTClient { +// +// func publishDewPoint( +// request: Client.SensorPublishRequest, +// state: State, +// topics: Topics +// ) -> EventLoopFuture<(MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics)> { +// guard let (dewPoint, topic) = request.dewPointData(topics: topics, units: state.units) +// else { +// 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)") +// return publish( +// to: topic, +// payload: ByteBufferAllocator().buffer(string: "\(roundedDewPoint)"), +// qos: .atLeastOnce, +// retain: true +// ) +// .map { (self, request, state, topics) } +// } +// } +// +// extension EventLoopFuture where Value == (Client.SensorPublishRequest, State) { +// func setHasProcessed() -> EventLoopFuture { +// map { request, state in +// request.setHasProcessed(state: state) +// } +// } +// } +// +// extension EventLoopFuture where Value == (MQTTNIO.MQTTClient, Client.SensorPublishRequest, State, Topics) { +// func publishEnthalpy() -> EventLoopFuture<(Client.SensorPublishRequest, State)> { +// flatMap { client, request, state, topics in +// guard let (enthalpy, topic) = request.enthalpyData(altitude: state.altitude, topics: topics, units: state.units) +// else { +// 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)") +// return client.publish( +// to: topic, +// payload: ByteBufferAllocator().buffer(string: "\(roundedEnthalpy)"), +// qos: .atLeastOnce +// ) +// .map { (request, state) } +// } +// } +// } diff --git a/Sources/ClientLive/Live.swift b/Sources/ClientLive/Live.swift index 068a983..a499f0e 100755 --- a/Sources/ClientLive/Live.swift +++ b/Sources/ClientLive/Live.swift @@ -1,41 +1,40 @@ -@_exported import Client -import CoreUnitTypes -import Foundation -import Models -import MQTTNIO -import NIO -import Psychrometrics - -public extension Client { - - // The state passed in here needs to be a class or we get escaping errors in the `addListeners` method. - static func live( - client: MQTTNIO.MQTTClient, - state: State, - topics: Topics - ) -> Self { - .init( - addListeners: { - state.addSensorListeners(to: client, topics: topics) - }, - connect: { - client.connect() - .map { _ in } - }, - publishSensor: { request in - client.publishDewPoint(request: request, state: state, topics: topics) - .publishEnthalpy() - .setHasProcessed() - }, - shutdown: { - client.disconnect() - .map { try? client.syncShutdownGracefully() } - }, - subscribe: { - // Sensor subscriptions - client.subscribe(to: .sensors(topics: topics)) - .map { _ in } - } - ) - } -} +// @_exported import Client +// import Foundation +// import Models +// import MQTTNIO +// import NIO +// import PsychrometricClient +// +// public extension Client { +// +// // The state passed in here needs to be a class or we get escaping errors in the `addListeners` method. +// static func live( +// client: MQTTNIO.MQTTClient, +// state: State, +// topics: Topics +// ) -> Self { +// .init( +// addListeners: { +// state.addSensorListeners(to: client, topics: topics) +// }, +// connect: { +// client.connect() +// .map { _ in } +// }, +// publishSensor: { request in +// client.publishDewPoint(request: request, state: state, topics: topics) +// .publishEnthalpy() +// .setHasProcessed() +// }, +// shutdown: { +// client.disconnect() +// .map { try? client.syncShutdownGracefully() } +// }, +// subscribe: { +// // Sensor subscriptions +// client.subscribe(to: .sensors(topics: topics)) +// .map { _ in } +// } +// ) +// } +// } diff --git a/Sources/ClientLive/SensorsClient.swift b/Sources/ClientLive/SensorsClient.swift index f95be3e..f77fedb 100644 --- a/Sources/ClientLive/SensorsClient.swift +++ b/Sources/ClientLive/SensorsClient.swift @@ -1,262 +1,262 @@ -import EnvVars -import Logging -import Models -import MQTTNIO -import NIO -import Psychrometrics -import ServiceLifecycle - -// TODO: Remove. -// TODO: Pass in eventLoopGroup and MQTTClient. -public actor SensorsClient { - - public static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - public let client: MQTTClient - public private(set) var shuttingDown: Bool - public private(set) var sensors: [TemperatureAndHumiditySensor] - - var logger: Logger { client.logger } - - public init( - envVars: EnvVars, - logger: Logger, - sensors: [TemperatureAndHumiditySensor] = [] - ) { - let config = MQTTClient.Configuration( - version: .v3_1_1, - userName: envVars.userName, - password: envVars.password, - useSSL: false, - useWebSockets: false, - tlsConfiguration: nil, - webSocketURLPath: nil - ) - self.client = MQTTClient( - host: envVars.host, - identifier: envVars.identifier, - eventLoopGroupProvider: .shared(Self.eventLoopGroup), - logger: logger, - configuration: config - ) - self.shuttingDown = false - self.sensors = sensors - } - - public func addSensor(_ sensor: TemperatureAndHumiditySensor) async throws { - guard sensors.firstIndex(where: { $0.location == sensor.location }) == nil else { - throw SensorExists() - } - sensors.append(sensor) - } - - 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() } - } +// import EnvVars +// import Logging +// import Models +// import MQTTNIO +// import NIO +// import PsychrometricClient +// import ServiceLifecycle +// +// // TODO: Remove. +// // TODO: Pass in eventLoopGroup and MQTTClient. +// public actor SensorsClient { +// +// public static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) +// public let client: MQTTClient +// public private(set) var shuttingDown: Bool +// public private(set) var sensors: [TemperatureAndHumiditySensor] +// +// var logger: Logger { client.logger } +// +// public init( +// envVars: EnvVars, +// logger: Logger, +// sensors: [TemperatureAndHumiditySensor] = [] +// ) { +// let config = MQTTClient.Configuration( +// version: .v3_1_1, +// userName: envVars.userName, +// password: envVars.password, +// useSSL: false, +// useWebSockets: false, +// tlsConfiguration: nil, +// webSocketURLPath: nil +// ) +// self.client = MQTTClient( +// host: envVars.host, +// identifier: envVars.identifier, +// eventLoopGroupProvider: .shared(Self.eventLoopGroup), +// logger: logger, +// configuration: config +// ) +// self.shuttingDown = false +// self.sensors = sensors +// } +// +// public func addSensor(_ sensor: TemperatureAndHumiditySensor) async throws { +// guard sensors.firstIndex(where: { $0.location == sensor.location }) == nil else { +// throw SensorExists() +// } +// sensors.append(sensor) +// } +// +// public func connect(cleanSession: Bool = true) async { // do { -// try await subscribeToSensors() -// try await addSensorListeners() -// logger.debug("Begin listening to sensors...") +// 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("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 +// } +// } catch { +// self.logger.trace("Error:\(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 // } - } - - 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:\n\(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:\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( - topic: String, - keyPath: WritableKeyPath, - 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 - } - -} +// } +// } +// +// 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( +// topic: String, +// keyPath: WritableKeyPath, +// 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 +// } +// +// } diff --git a/Sources/Models/Mode.swift b/Sources/Models/Mode.swift index d093e73..d54b448 100755 --- a/Sources/Models/Mode.swift +++ b/Sources/Models/Mode.swift @@ -1,4 +1,4 @@ -import CoreUnitTypes +import PsychrometricClient // TODO: Remove @@ -21,7 +21,7 @@ public enum Mode: Equatable { public enum HumidifyMode: Equatable { /// Control humidifying based off dew-point. - case dewPoint(Temperature) + case dewPoint(DewPoint) /// Control humidifying based off relative humidity. case relativeHumidity(RelativeHumidity) @@ -31,7 +31,7 @@ public enum Mode: Equatable { public enum DehumidifyMode: Equatable { /// Control dehumidifying based off dew-point. - case dewPoint(high: Temperature, low: Temperature) + case dewPoint(high: DewPoint, low: DewPoint) /// Control humidifying based off relative humidity. case relativeHumidity(high: RelativeHumidity, low: RelativeHumidity) diff --git a/Sources/Models/State.swift b/Sources/Models/State.swift index d3246da..ddaf804 100755 --- a/Sources/Models/State.swift +++ b/Sources/Models/State.swift @@ -1,5 +1,5 @@ import Foundation -@preconcurrency import Psychrometrics +import PsychrometricClient // TODO: Remove // 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 sensors: Sensors - public var units: PsychrometricEnvironment.Units { - didSet { - PsychrometricEnvironment.shared.units = units - } - } + public var units: PsychrometricUnits public init( altitude: Length = .seaLevel, sensors: Sensors = .init(), - units: PsychrometricEnvironment.Units = .imperial + units: PsychrometricUnits = .imperial ) { self.altitude = altitude self.sensors = sensors @@ -56,7 +52,7 @@ public extension State.Sensors { struct TemperatureHumiditySensor: Equatable { @TrackedChanges - public var temperature: Temperature? + public var temperature: DryBulb? @TrackedChanges 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, - let humidity = humidity, - !temperature.rawValue.isNaN, - !humidity.rawValue.isNaN + let humidity = humidity 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? { + // WARN: Fix me. + public func enthalpy(altitude _: Length, units _: PsychrometricUnits? = nil) -> EnthalpyOf? { guard let temperature = temperature, - let humidity = humidity, - !temperature.rawValue.isNaN, - !humidity.rawValue.isNaN + let humidity = humidity 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( - temperature: Temperature? = nil, + temperature: DryBulb? = nil, humidity: RelativeHumidity? = nil, needsProcessed: Bool = false ) { diff --git a/Sources/Models/TemperatureAndHumiditySensor.swift b/Sources/Models/TemperatureAndHumiditySensor.swift index f1675b2..e97f777 100644 --- a/Sources/Models/TemperatureAndHumiditySensor.swift +++ b/Sources/Models/TemperatureAndHumiditySensor.swift @@ -1,10 +1,13 @@ -@preconcurrency import Psychrometrics +import Dependencies +import PsychrometricClient /// Represents a temperature and humidity sensor that can be used to derive /// the dew-point temperature and enthalpy values. /// /// > 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. public var id: Location { location } @@ -21,7 +24,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @ /// The current temperature value of the sensor. @TrackedChanges - public var temperature: Temperature? + public var temperature: DryBulb? /// The topics to listen for updated sensor values. public let topics: Topics @@ -37,7 +40,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @ public init( location: Location, altitude: Length = .feet(800.0), - temperature: Temperature? = nil, + temperature: DryBulb? = nil, humidity: RelativeHumidity? = nil, needsProcessed: Bool = false, topics: Topics? = nil @@ -51,22 +54,26 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @ /// The calculated dew-point temperature of the sensor. public var dewPoint: DewPoint? { - guard let temperature = temperature, - let humidity = humidity, - !temperature.rawValue.isNaN, - !humidity.rawValue.isNaN - else { return nil } - return .init(dryBulb: temperature, humidity: humidity) + get async { + guard let temperature = temperature, + let humidity = humidity + else { return nil } + return try? await psychrometrics.dewPoint(.dryBulb(temperature, relativeHumidity: humidity)) + // return .init(dryBulb: temperature, humidity: humidity) + } } /// The calculated enthalpy of the sensor. public var enthalpy: EnthalpyOf? { - guard let temperature = temperature, - let humidity = humidity, - !temperature.rawValue.isNaN, - !humidity.rawValue.isNaN - else { return nil } - return .init(dryBulb: temperature, humidity: humidity, altitude: altitude) + get async { + guard let temperature = temperature, + let humidity = humidity + else { return nil } + 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. @@ -82,7 +89,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @ /// 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. - public enum Location: String, CaseIterable, Equatable, Hashable { + public enum Location: String, CaseIterable, Equatable, Hashable, Sendable { case mixedAir = "mixed_air" case postCoil = "post_coil" case `return` @@ -90,7 +97,7 @@ public struct TemperatureAndHumiditySensor: Equatable, Hashable, Identifiable, @ } /// 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. public let dewPoint: String diff --git a/Sources/Models/TrackedChanges.swift b/Sources/Models/TrackedChanges.swift index 800b505..6114ac4 100755 --- a/Sources/Models/TrackedChanges.swift +++ b/Sources/Models/TrackedChanges.swift @@ -96,3 +96,5 @@ extension TrackedChanges: Hashable where Value: Hashable { hasher.combine(needsProcessed) } } + +extension TrackedChanges: Sendable where Value: Sendable {} diff --git a/Sources/SensorsService/Helpers.swift b/Sources/SensorsService/Helpers.swift index 1817e34..fd336c8 100755 --- a/Sources/SensorsService/Helpers.swift +++ b/Sources/SensorsService/Helpers.swift @@ -1,10 +1,9 @@ -import CoreUnitTypes import Logging import Models import MQTTNIO import NIO import NIOFoundationCompat -import Psychrometrics +import SharedModels /// Represents a type that can be initialized by a ``ByteBuffer``. protocol BufferInitalizable { @@ -24,18 +23,39 @@ extension Double: BufferInitalizable { } } -extension Temperature: BufferInitalizable { - /// Attempt to create / parse a temperature from a byte buffer. +// extension DryBulb: 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(.init(value, units: .celsius)) +// } +// } + +extension Tagged: BufferInitalizable where RawValue: BufferInitalizable { init?(buffer: inout ByteBuffer) { - guard let value = Double(buffer: &buffer) else { return nil } - self.init(value, units: .celsius) + guard let value = RawValue(buffer: &buffer) else { return nil } + self.init(value) } } -extension RelativeHumidity: BufferInitalizable { - /// Attempt to create / parse a relative humidity from a byte buffer. +extension Humidity: BufferInitalizable { init?(buffer: inout ByteBuffer) { guard let value = Double(buffer: &buffer) else { return nil } self.init(value) } } + +extension Temperature: 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) +// } +// } diff --git a/Sources/SensorsService/SensorsService.swift b/Sources/SensorsService/SensorsService.swift index e6b2d2b..e9d8fcf 100644 --- a/Sources/SensorsService/SensorsService.swift +++ b/Sources/SensorsService/SensorsService.swift @@ -3,7 +3,7 @@ import Logging import Models @preconcurrency import MQTTNIO import NIO -import Psychrometrics +import PsychrometricClient import ServiceLifecycle public actor SensorsService: Service { @@ -52,7 +52,7 @@ public actor SensorsService: Service { if topic.contains("temperature") { // do something. 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)") throw DecodingError() } @@ -96,8 +96,8 @@ public actor SensorsService: Service { 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 await publish(double: sensor.dewPoint?.value, to: sensor.topics.dewPoint) + try await publish(double: sensor.enthalpy?.value, to: sensor.topics.enthalpy) try sensors.hasProcessed(sensor) } } diff --git a/Sources/dewPoint-controller/main.swift b/Sources/dewPoint-controller/main.swift index 2e0d12f..c82840e 100755 --- a/Sources/dewPoint-controller/main.swift +++ b/Sources/dewPoint-controller/main.swift @@ -1,74 +1,74 @@ -import Bootstrap -import ClientLive -import CoreUnitTypes -import Logging -import Models -import MQTTNIO -import NIO -import TopicsLive -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...") +// import Bootstrap +// import ClientLive +// import CoreUnitTypes +// import Logging +// import Models +// import MQTTNIO +// import NIO +// import TopicsLive +// import Foundation // -// logger.debug("Published dew point...") - - Thread.sleep(forTimeInterval: 5) -} +// 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...") +// +// Thread.sleep(forTimeInterval: 5) +// }