feat: Adds psychrometrics calculator.

This commit is contained in:
2025-03-06 13:41:38 -05:00
parent e0e5b10a34
commit ee577003d5
13 changed files with 1386 additions and 7 deletions

View File

@@ -0,0 +1,106 @@
import CasePaths
import PsychrometricClient
@preconcurrency import URLRouting
public enum Psychrometrics {
public static let description = """
Calculate the psychrometric properties of air based on temperature and humidity readings.
"""
public struct Request: Codable, Equatable, Sendable {
// public let mode: Mode
public let temperature: Double
public let humidity: Double
public let altitude: Double?
public init(temperature: Double, humidity: Double, altitude: Double? = nil) {
self.temperature = temperature
self.humidity = humidity
self.altitude = altitude
}
public enum FieldKey: String {
case temperature
case humidity
case altitude
}
}
public struct Response: Codable, Equatable, Sendable {
public let properties: PsychrometricProperties
public let warnings: [String]
public init(properties: PsychrometricProperties, warnings: [String]) {
self.properties = properties
self.warnings = warnings
}
}
}
// MARK: - Router
public extension SiteRoute.View {
enum Psychrometrics: Equatable, Sendable {
case index
case submit(Routes.Psychrometrics.Request)
typealias Key = Routes.Psychrometrics.Request.FieldKey
static let rootPath = "psychrometric-properties"
public static let router = OneOf {
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.submit)) {
Path { rootPath }
Method.post
Body {
FormData {
Field(Key.temperature) {
Double.parser()
}
Field(Key.humidity) {
Double.parser()
}
Optionally {
Field(Key.altitude) {
Double.parser()
}
}
}
}
.map(.memberwise(Routes.Psychrometrics.Request.init))
}
}
}
}
#if DEBUG
public extension Psychrometrics.Response {
static let mock = Self(
properties: .init(
absoluteHumidity: 91.4,
atmosphericPressure: 0.3,
degreeOfSaturation: 0.66,
density: 0.07,
dewPoint: 64.3,
dryBulb: 76,
enthalpy: 32.33,
grainsOfMoisture: 91.4,
humidityRatio: 0.01,
relativeHumidity: 67,
specificVolume: 13.78,
vaporPressure: 0.3,
wetBulb: 67.7,
units: .imperial
),
warnings: [
"Test warning."
]
)
}
#endif

View File

@@ -4,6 +4,9 @@ import Foundation
import PsychrometricClient
@preconcurrency import URLRouting
// FIX: Move Routers into their respective model files, to reduce the size / complexity of this file
// and keep them closer to where changes may need made.
// swiftlint:disable type_body_length
public enum SiteRoute: Equatable, Sendable {
@@ -105,6 +108,7 @@ public extension SiteRoute {
case heatingBalancePoint(HeatingBalancePoint)
case hvacSystemPerformance(HVACSystemPerformance)
case moldRisk(MoldRisk)
case psychrometrics(Self.Psychrometrics)
case roomPressure(RoomPressure)
public static let router = OneOf {
@@ -132,6 +136,9 @@ public extension SiteRoute {
Route(.case(Self.moldRisk)) {
MoldRisk.router
}
Route(.case(Self.psychrometrics)) {
Self.Psychrometrics.router
}
Route(.case(Self.roomPressure)) {
RoomPressure.router
}
@@ -460,6 +467,7 @@ public extension SiteRoute {
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
import URLRouting
// Allow the use of a field key enum as the `name` parameter, to avoid
// stringly type name fields.
public extension Field {
@inlinable
init<Key>(
_ name: Key,
default defaultValue: Value.Output? = nil,
@ParserBuilder<Substring> _ value: () -> Value
) where Key: RawRepresentable, Key.RawValue == String {
self.init(name.rawValue, default: defaultValue, value)
}
@inlinable
init<Key, C>(
_ name: Key,
_ value: C,
default defaultValue: Value.Output? = nil,
) where Key: RawRepresentable, Key.RawValue == String,
Value == Parsers.MapConversion<Parsers.ReplaceError<Rest<Substring>>, C>
{
self.init(name.rawValue, value, default: defaultValue)
}
@inlinable
init<Key>(
_ name: Key,
default defaultValue: Value.Output? = nil
)
where
Key: RawRepresentable, Key.RawValue == String,
Value == Parsers.MapConversion<
Parsers.ReplaceError<Rest<Substring>>, Conversions.SubstringToString
>
{
self.init(name.rawValue, default: defaultValue)
}
}