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

@@ -1,17 +0,0 @@
import Foundation
private let numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
return formatter
}()
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))!)
}
}

View File

@@ -76,6 +76,15 @@ extension ViewController: DependencyKey {
}
}
case let .atticVentilation(route):
switch route {
case .index:
return request.respond(AtticVentilationForm(response: nil))
case let .submit(request):
let response = try await request.respond(logger: logger)
return AtticVentilationResponse(response: response)
}
case let .capacitor(route):
switch route {
case let .index(mode: mode):

View File

@@ -67,6 +67,7 @@ private struct Header: HTML {
navLink(label: "HVAC-System-Performance", route: .hvacSystemPerformance(.index))
navLink(label: "Room-Pressure", route: .roomPressure(.index))
navLink(label: "Capcitor-Calculator", route: .capacitor(.index))
navLink(label: "Attic-Ventilation", route: .atticVentilation(.index))
}
}
}

View File

@@ -0,0 +1,182 @@
import Elementary
import ElementaryHTMX
import Routes
import Styleguide
struct AtticVentilationForm: HTML, Sendable {
let response: AtticVentilation.Response?
var content: some HTML {
FormHeader(label: "Attic Ventilation Calculator", svg: .wind)
// Form.
form(
.hx.post(route: .atticVentilation(.index)),
.hx.target("#result")
) {
div(.class("space-y-6")) {
div {
LabeledContent(label: "Pressure Differential: (Pascals - WRT outside)") {
Input(id: "pressureDifferential", placeholder: "Pressure differential")
.attributes(.type(.number), .step("0.1"), .autofocus, .required)
}
p(.class("text-sm text-light mt-2")) { "Positive values indicate higher attic pressure" }
}
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
div {
p(.class("font-bold mb-6")) { "Outdoor Conditions" }
LabeledContent(label: "Temperature: (°F)") {
Input(id: "outdoorTemperature", placeholder: "Outdoor temperature")
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .required)
}
LabeledContent(label: "Dew Point: (°F)") {
Input(id: "outdoorDewpoint", placeholder: "Outdoor dewpoint")
.attributes(.type(.number), .step("0.1"), .required)
}
}
div {
p(.class("font-bold mb-6")) { "Attic Conditions" }
LabeledContent(label: "Temperature: (°F)") {
Input(id: "atticTemperature", placeholder: "Attic temperature")
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .required)
}
LabeledContent(label: "Dew Point: (°F)") {
Input(id: "atticDewpoint", placeholder: "Attic dewpoint")
.attributes(.type(.number), .step("0.1"), .required)
}
}
}
LabeledContent(label: "Attic Floor Area: (ft²)") {
Input(id: "atticFloorArea", placeholder: "Attic floor area")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
LabeledContent(label: "Existing Intake Area: (ft²)") {
Input(id: "existingIntakeArea", placeholder: "Intake area (optional)")
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .min("0.1"), .required)
}
LabeledContent(label: "Existing Exhaust Area: (ft²)") {
Input(id: "existingExhaustArea", placeholder: "Exhaust area (optional)")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
}
}
div {
SubmitButton(label: "Calculate Ventilation")
}
}
}
div(.id("result")) {
if let response {
AtticVentilationResponse(response: response)
}
}
}
}
struct AtticVentilationResponse: HTML, Sendable {
let response: AtticVentilation.Response
var content: some HTML {
ResultContainer(reset: .atticVentilation(.index)) {
div(.class("space-y-6")) {
statusView()
requiredVentilationView()
WarningBox(warnings: response.warnings)
Note {
"""
Calculations are based on standard ventilation guidelines and building codes.
Local codes may vary. Consider consulting with a qualified professional for specific
recommendations.
"""
}
}
}
}
private func statusView() -> some HTML {
div(
.class("""
w-full rounded-lg shadow-lg border-2 p-6 \(response.ventilationStatus.borderColor)
\(response.ventilationStatus.textColor) \(response.ventilationStatus.backgroundColor)
""")
) {
div(.class("flex text-2xl mb-6")) {
SVG(.thermometerSun, color: response.ventilationStatus.textColor)
h4(.class("font-extrabold ms-2")) { "Ventilation Status: \(response.ventilationStatus.rawValue.capitalized)" }
}
div(.class("grid justify-items-stretch grid-cols-1 md:grid-cols-3")) {
group("Temperature Differential", "\(double: response.temperatureDifferential, fractionDigits: 1) °F")
group("Dewpoint Differential", "\(double: response.dewpointDifferential, fractionDigits: 1) °F")
group("Pressure Differential", "\(double: response.pressureDifferential, fractionDigits: 1) °F")
}
}
}
private func requiredVentilationView() -> some HTML {
div(
.class("""
w-full rounded-lg border p-6 border-blue-600 text-blue-600 bg-blue-100 dark:bg-blue-300
""")
) {
div(.class("flex text-2xl mb-6")) {
h4(.class("font-extrabold ms-2")) { "Required Ventilation" }
}
div(.class("grid grid-cols-1 md:grid-cols-2 content-center")) {
group("Intake", "\(double: response.requiredVentilationArea.intake, fractionDigits: 1) ft²")
group("Exhaust", "\(double: response.requiredVentilationArea.exhaust, fractionDigits: 1) ft²")
}
if response.recommendations.count > 0 {
div(.class("mt-8")) {
h4(.class("font-bold")) { "Recommendations:" }
ul(.class("list-disc mx-10")) {
for recommendation in response.recommendations {
li { recommendation }
}
}
}
}
}
}
private func group(_ label: String, _ value: String) -> some HTML<HTMLTag.div> {
div(.class("justify-self-center")) {
div(.class("grid grid-cols-1 justify-items-center")) {
p(.class("font-medium")) { label }
h3(.class("text-3xl font-extrabold")) { value }
}
}
}
}
private extension AtticVentilation.VentilationStatus {
var backgroundColor: String {
switch self {
case .adequate: return "bg-green-200"
case .inadequate: return "bg-amber-200"
case .critical: return "bg-red-200"
}
}
var borderColor: String {
switch self {
case .adequate: return "border-green-500"
case .inadequate: return "border-amber-500"
case .critical: return "border-red-500"
}
}
var textColor: String {
switch self {
case .adequate: return "text-green-500"
case .inadequate: return "text-amber-500"
case .critical: return "text-red-500"
}
}
}

View File

@@ -249,8 +249,8 @@ private extension RoomPressure.Mode {
var label: String {
switch self {
case let .knownAirflow: return "Known Airflow"
case let .measuredPressure: return "Measured Pressure"
case .knownAirflow: return "Known Airflow"
case .measuredPressure: return "Measured Pressure"
}
}
}