feat: Adds attic ventilation calculation.

This commit is contained in:
2025-03-01 22:24:06 -05:00
parent 3c7147ad0e
commit 0ff7d6666c
11 changed files with 509 additions and 5 deletions

View 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

View File

@@ -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)

View 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))!)
}
}