feat: Adds attic ventilation calculation.
This commit is contained in:
102
Sources/Routes/Models/AtticVentilation.swift
Normal file
102
Sources/Routes/Models/AtticVentilation.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
public enum AtticVentilation {
|
||||
|
||||
public struct Request: Codable, Equatable, Sendable {
|
||||
|
||||
public let pressureDifferential: Double
|
||||
public let outdoorTemperature: Double
|
||||
public let outdoorDewpoint: Double
|
||||
public let atticTemperature: Double
|
||||
public let atticDewpoint: Double
|
||||
public let atticFloorArea: Double
|
||||
public let existingIntakeArea: Double?
|
||||
public let existingExhaustArea: Double?
|
||||
|
||||
public init(
|
||||
pressureDifferential: Double,
|
||||
outdoorTemperature: Double,
|
||||
outdoorDewpoint: Double,
|
||||
atticTemperature: Double,
|
||||
atticDewpoint: Double,
|
||||
atticFloorArea: Double,
|
||||
existingIntakeArea: Double? = nil,
|
||||
existingExhaustArea: Double? = nil
|
||||
) {
|
||||
self.pressureDifferential = pressureDifferential
|
||||
self.outdoorTemperature = outdoorTemperature
|
||||
self.outdoorDewpoint = outdoorDewpoint
|
||||
self.atticTemperature = atticTemperature
|
||||
self.atticDewpoint = atticDewpoint
|
||||
self.atticFloorArea = atticFloorArea
|
||||
self.existingIntakeArea = existingIntakeArea
|
||||
self.existingExhaustArea = existingExhaustArea
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable, Equatable, Sendable {
|
||||
|
||||
public let pressureDifferential: Double
|
||||
public let temperatureDifferential: Double
|
||||
public let dewpointDifferential: Double
|
||||
public let stackEffect: Double
|
||||
public let requiredVentilationArea: RequiredVentilationArea
|
||||
public let recommendations: [String]
|
||||
public let warnings: [String]
|
||||
public let ventilationStatus: VentilationStatus
|
||||
|
||||
public init(
|
||||
pressureDifferential: Double,
|
||||
temperatureDifferential: Double,
|
||||
dewpointDifferential: Double,
|
||||
stackEffect: Double,
|
||||
requiredVentilationArea: AtticVentilation.RequiredVentilationArea,
|
||||
recommendations: [String],
|
||||
warnings: [String],
|
||||
ventilationStatus: AtticVentilation.VentilationStatus
|
||||
) {
|
||||
self.pressureDifferential = pressureDifferential
|
||||
self.temperatureDifferential = temperatureDifferential
|
||||
self.dewpointDifferential = dewpointDifferential
|
||||
self.stackEffect = stackEffect
|
||||
self.requiredVentilationArea = requiredVentilationArea
|
||||
self.recommendations = recommendations
|
||||
self.warnings = warnings
|
||||
self.ventilationStatus = ventilationStatus
|
||||
}
|
||||
}
|
||||
|
||||
public enum VentilationStatus: String, Codable, CaseIterable, Equatable, Sendable {
|
||||
case adequate
|
||||
case inadequate
|
||||
case critical
|
||||
}
|
||||
|
||||
public struct RequiredVentilationArea: Codable, Equatable, Sendable {
|
||||
|
||||
public let intake: Double
|
||||
public let exhaust: Double
|
||||
|
||||
public init(intake: Double, exhaust: Double) {
|
||||
self.intake = intake
|
||||
self.exhaust = exhaust
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public extension AtticVentilation.Response {
|
||||
static let mock = Self(
|
||||
pressureDifferential: 4,
|
||||
temperatureDifferential: 15,
|
||||
dewpointDifferential: 10,
|
||||
stackEffect: 2,
|
||||
requiredVentilationArea: .init(intake: 4.9, exhaust: 3.3),
|
||||
recommendations: [
|
||||
"Install 4.9 sq. ft. of intake ventilation",
|
||||
"Install 3.3 sq. ft. of exhaust ventilation",
|
||||
"Consider adding exhaust ventilation to balance pressure"
|
||||
],
|
||||
warnings: ["High pressure differential - may indicate blocked vents or insufficient ventilation."],
|
||||
ventilationStatus: AtticVentilation.VentilationStatus.allCases.randomElement()!
|
||||
)
|
||||
}
|
||||
#endif
|
||||
@@ -23,6 +23,7 @@ public extension SiteRoute {
|
||||
|
||||
enum Api: Equatable, Sendable {
|
||||
|
||||
case calculateAtticVentilation(AtticVentilation.Request)
|
||||
case calculateCapacitor(Capacitor.Request)
|
||||
case calculateDehumidifierSize(DehumidifierSize.Request)
|
||||
case calculateHVACSystemPerformance(HVACSystemPerformance.Request)
|
||||
@@ -32,6 +33,11 @@ public extension SiteRoute {
|
||||
static let rootPath = Path { "api"; "v1" }
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.calculateAtticVentilation)) {
|
||||
Path { "api"; "v1"; "calculateAtticPressure" }
|
||||
Method.post
|
||||
Body(.json(AtticVentilation.Request.self))
|
||||
}
|
||||
Route(.case(Self.calculateCapacitor)) {
|
||||
Path { "api"; "v1"; "calculateRoomPressure" }
|
||||
Method.post
|
||||
@@ -75,6 +81,7 @@ public extension SiteRoute {
|
||||
enum View: Equatable, Sendable {
|
||||
|
||||
case index
|
||||
case atticVentilation(AtticVentilation)
|
||||
case capacitor(Capacitor)
|
||||
case dehumidifierSize(DehumidifierSize)
|
||||
case hvacSystemPerformance(HVACSystemPerformance)
|
||||
@@ -85,6 +92,9 @@ public extension SiteRoute {
|
||||
Route(.case(Self.index)) {
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.atticVentilation)) {
|
||||
AtticVentilation.router
|
||||
}
|
||||
Route(.case(Self.capacitor)) {
|
||||
Capacitor.router
|
||||
}
|
||||
@@ -102,6 +112,37 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum AtticVentilation: Equatable, Sendable {
|
||||
case index
|
||||
case submit(Routes.AtticVentilation.Request)
|
||||
|
||||
static let rootPath = "attic-ventilation"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.submit)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body {
|
||||
FormData {
|
||||
Field("pressureDifferential") { Double.parser() }
|
||||
Field("outdoorTemperature") { Double.parser() }
|
||||
Field("outdoorDewpoint") { Double.parser() }
|
||||
Field("atticTemperature") { Double.parser() }
|
||||
Field("atticDewpoint") { Double.parser() }
|
||||
Field("atticFloorArea") { Double.parser() }
|
||||
Optionally { Field("existingIntakeArea") { Double.parser() } }
|
||||
Optionally { Field("existingExhaustArea") { Double.parser() } }
|
||||
}
|
||||
.map(.memberwise(Routes.AtticVentilation.Request.init))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Capacitor: Equatable, Sendable {
|
||||
case index(mode: Routes.Capacitor.Mode? = nil)
|
||||
case submit(Routes.Capacitor.Request)
|
||||
|
||||
17
Sources/Routes/String+double.swift
Normal file
17
Sources/Routes/String+double.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
private let numberFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = 2
|
||||
return formatter
|
||||
}()
|
||||
|
||||
public extension String.StringInterpolation {
|
||||
mutating func appendInterpolation(double: Double, fractionDigits: Int = 2) {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = fractionDigits
|
||||
appendInterpolation(numberFormatter.string(from: NSNumber(value: double))!)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user