feat: Begins room-pressure calculator
This commit is contained in:
@@ -2,6 +2,10 @@ import Elementary
|
||||
|
||||
extension HTMLAttribute where Tag == HTMLTag.input {
|
||||
|
||||
static func max(_ value: String) -> Self {
|
||||
.init(name: "max", value: value)
|
||||
}
|
||||
|
||||
static func min(_ value: String) -> Self {
|
||||
.init(name: "min", value: value)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,8 @@ extension ViewController: DependencyKey {
|
||||
case let .hvacSystemPerformance(route):
|
||||
switch route {
|
||||
case .index:
|
||||
return request.respond(HVACSystemPerformanceForm())
|
||||
// let response = try await HVACSystemPerformance.Response.mock()
|
||||
return request.respond(HVACSystemPerformanceForm(response: nil))
|
||||
case let .submit(hvacRequest):
|
||||
let response = try await hvacRequest.respond(logger: request.logger)
|
||||
return HVACSystemPerformanceResult(response: response)
|
||||
@@ -95,6 +96,12 @@ extension ViewController: DependencyKey {
|
||||
let response = try await psychrometricClient.respond(request)
|
||||
return MoldRiskResponse(response: response)
|
||||
}
|
||||
|
||||
case let .roomPressure(route):
|
||||
switch route {
|
||||
case let .index(mode):
|
||||
return request.respond(RoomPressureForm(mode: mode, response: nil))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,6 +153,15 @@ struct HVACSystemPerformanceResult: HTML, Sendable {
|
||||
LabeledMetric(label: "Sensible Heat Ratio", value: shr, units: "%")
|
||||
}
|
||||
}
|
||||
|
||||
if shr < 0.7 {
|
||||
div(.class("mx-8 mb-4 mt-8 p-4 rounded-xl shadow-lg border border-amber-500 bg-amber-200 text-amber-500")) {
|
||||
h4(.class("text-lg font-semibold")) { "Warning:" }
|
||||
p(.class("text-sm")) {
|
||||
"Low sensible heat ratio may indicate excessive dehumidification or low airflow."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,14 @@ private struct Header: HTML {
|
||||
"HVAC-System-Performance"
|
||||
}
|
||||
}
|
||||
li {
|
||||
a(
|
||||
.class("hover:border-b \(border: .yellow)"),
|
||||
.hx.get(route: .roomPressure(.index)), .hx.target("#content"), .hx.pushURL(true)
|
||||
) {
|
||||
"Room-Pressure"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@ struct MoldRiskResponse: HTML {
|
||||
}
|
||||
|
||||
// Display psychrometric properties.
|
||||
PsychrometricPropertiesGrid(properties: response.psychrometricProperties)
|
||||
.attributes(.class("mx-6"))
|
||||
Properties(properties: response.psychrometricProperties)
|
||||
.attributes(.class("mt-8"))
|
||||
|
||||
// Disclaimer.
|
||||
Note {
|
||||
@@ -98,6 +98,20 @@ struct MoldRiskResponse: HTML {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Properties: HTML, Sendable {
|
||||
let properties: PsychrometricProperties
|
||||
|
||||
var content: some HTML<HTMLTag.div> {
|
||||
div(.class("w-full rounded-lg border")) {
|
||||
h3(.class("flex justify-center text-xl font-semibold mb-6 mt-2")) { "Psychrometric Properties" }
|
||||
PsychrometricPropertiesView(properties: properties)
|
||||
.attributes(.class("""
|
||||
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2 px-4 pb-4
|
||||
"""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension MoldRisk.RiskLevel {
|
||||
@@ -129,46 +143,3 @@ private extension MoldRisk.RiskLevel {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: Remove and use PsychrometricPropertiesView as the base.
|
||||
struct PsychrometricPropertiesGrid: HTML {
|
||||
let properties: PsychrometricProperties
|
||||
|
||||
var content: some HTML<HTMLTag.div> {
|
||||
div(.class("pt-8")) {
|
||||
p(.class("text-xl font-semibold border-b")) {
|
||||
"Psychrometric Properties:"
|
||||
}
|
||||
div(.class("w-full mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")) {
|
||||
displayProperty("Dew Point", \.dewPoint.rawValue)
|
||||
displayProperty("Wet Bulb", \.wetBulb.rawValue)
|
||||
displayProperty("Enthalpy", \.enthalpy.rawValue)
|
||||
displayProperty("Density", \.density.rawValue)
|
||||
displayProperty("Vapor Pressure", \.vaporPressure.rawValue)
|
||||
displayProperty("Specific Volume", properties.specificVolume.rawValue)
|
||||
displayProperty("Absolute Humidity", \.absoluteHumidity)
|
||||
displayProperty("Humidity Ratio", properties.humidityRatio.value)
|
||||
displayProperty("Degree of Saturation", properties.degreeOfSaturation.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func displayProperty(_ label: String, _ value: Double, _ symbol: String? = nil) -> some HTML {
|
||||
let symbol = "\(symbol == nil ? "" : " \(symbol!)")"
|
||||
|
||||
return p(.class("text-blue-500 dark:text-slate-200")) {
|
||||
span(.class("font-semibold")) { "\(label): " }
|
||||
span(.class("font-light")) {
|
||||
"\(double: value)\(symbol)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func displayProperty<N: NumberWithUnitOfMeasure>(
|
||||
_ label: String,
|
||||
_ keyPath: KeyPath<PsychrometricProperties, N>
|
||||
) -> some HTML where N.Number == Double, N.Units: RawRepresentable, N.Units.RawValue == String {
|
||||
let property = properties[keyPath: keyPath]
|
||||
return displayProperty(label, property.rawValue, property.units.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
166
Sources/ViewController/Views/RoomPressure.swift
Normal file
166
Sources/ViewController/Views/RoomPressure.swift
Normal file
@@ -0,0 +1,166 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Routes
|
||||
import Styleguide
|
||||
|
||||
struct RoomPressureForm: HTML, Sendable {
|
||||
let response: RoomPressure.Response?
|
||||
let mode: RoomPressure.Mode
|
||||
|
||||
init(mode: RoomPressure.Mode? = nil, response: RoomPressure.Response? = nil) {
|
||||
self.mode = mode ?? .knownAirflow
|
||||
self.response = response
|
||||
}
|
||||
|
||||
var content: some HTML {
|
||||
div(.class("relative")) {
|
||||
FormHeader(label: "Room Pressure Calculator", svg: .leftRightArrow)
|
||||
|
||||
// Mode toggle / buttons.
|
||||
div(.class("absolute top-0 right-0 flex items-center gap-x-0")) {
|
||||
switch mode {
|
||||
case .knownAirflow:
|
||||
SecondaryButton(label: "Known Airflow")
|
||||
.attributes(.class("rounded-s-lg"))
|
||||
.attributes(.disabled, when: mode == .knownAirflow)
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .knownAirflow))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .measuredPressure
|
||||
)
|
||||
PrimaryButton(label: "Measured Pressure")
|
||||
.attributes(.class("rounded-e-lg"))
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .measuredPressure))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .knownAirflow
|
||||
)
|
||||
case .measuredPressure:
|
||||
PrimaryButton(label: "Known Airflow")
|
||||
.attributes(.class("rounded-s-lg"))
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .knownAirflow))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .measuredPressure
|
||||
)
|
||||
SecondaryButton(label: "Measured Pressure")
|
||||
.attributes(.class("rounded-e-lg"))
|
||||
.attributes(.disabled, when: mode == .measuredPressure)
|
||||
.attributes(
|
||||
.hx.get(route: .roomPressure(.index(mode: .measuredPressure))),
|
||||
.hx.target("#content"),
|
||||
when: mode == .knownAirflow
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Form(mode: mode)
|
||||
|
||||
div(.id("result")) {
|
||||
if let response {
|
||||
RoomPressureResult(response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Form: HTML, Sendable {
|
||||
let mode: RoomPressure.Mode
|
||||
|
||||
var content: some HTML<HTMLTag.form> {
|
||||
form {
|
||||
div(.class("space-y-6")) {
|
||||
LabeledContent(label: pressureLabel) {
|
||||
// NB: using .attributes(..., when:...) not working, so using a switch statement.
|
||||
switch mode {
|
||||
case .knownAirflow:
|
||||
Input(id: pressureID, placeholder: pressurePlaceholder)
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .max("3.0"), .autofocus, .required)
|
||||
case .measuredPressure:
|
||||
Input(id: pressureID, placeholder: pressurePlaceholder)
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus, .required)
|
||||
}
|
||||
}
|
||||
|
||||
DoorDetails()
|
||||
|
||||
if mode == .knownAirflow {
|
||||
LabeledContent(label: "Supply Airflow (CFM)") {
|
||||
Input(id: "supplyAirflow", placeholder: "Airflow")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
}
|
||||
|
||||
PreferredGrilleHeight()
|
||||
|
||||
div {
|
||||
SubmitButton(label: "Calculate Return Path Size")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var pressureLabel: String {
|
||||
switch mode {
|
||||
case .knownAirflow: return "Target Room Pressure (Pascals)"
|
||||
case .measuredPressure: return "Measured Room Pressure (Pascals)"
|
||||
}
|
||||
}
|
||||
|
||||
private var pressureID: String {
|
||||
switch mode {
|
||||
case .knownAirflow: return "targetRoomPressure"
|
||||
case .measuredPressure: return "measuredRoomPressure"
|
||||
}
|
||||
}
|
||||
|
||||
private var pressurePlaceholder: String {
|
||||
switch mode {
|
||||
case .knownAirflow: return "Room pressure (max 3 pa.)"
|
||||
case .measuredPressure: return "Measure pressure"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DoorDetails: HTML, Sendable {
|
||||
|
||||
var content: some HTML {
|
||||
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-6")) {
|
||||
LabeledContent(label: "Door Width (in.)") {
|
||||
Input(id: "doorWidth", placeholder: "Width")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
|
||||
LabeledContent(label: "Door Height (in.)") {
|
||||
Input(id: "doorHeight", placeholder: "Height")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent(label: "Door Undercut (in.)") {
|
||||
Input(id: "doorUndercut", placeholder: "Undercut height")
|
||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PreferredGrilleHeight: HTML, Sendable {
|
||||
|
||||
var content: some HTML {
|
||||
InputLabel(for: "preferredGrilleHeight") { "Preferred Grille Height" }
|
||||
select(
|
||||
.id("preferredGrilleHeight"),
|
||||
.name("preferredGrilleHeight"),
|
||||
.class("w-full px-4 py-2 rounded-md border")
|
||||
) {
|
||||
for height in RoomPressure.CommonReturnGrilleHeight.allCases {
|
||||
option(.value("\(height.rawValue)")) { height.label }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomPressureResult: HTML, Sendable {
|
||||
let response: RoomPressure.Response
|
||||
}
|
||||
Reference in New Issue
Block a user