feat: Begins room-pressure calculator
This commit is contained in:
@@ -61,7 +61,8 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
|
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
|
||||||
# libcurl4 \
|
libcurl4 \
|
||||||
|
curl \
|
||||||
# If your app or its dependencies import FoundationXML, also install `libxml2`.
|
# If your app or its dependencies import FoundationXML, also install `libxml2`.
|
||||||
# libxml2 \
|
# libxml2 \
|
||||||
&& rm -r /var/lib/apt/lists/*
|
&& rm -r /var/lib/apt/lists/*
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
31
Sources/Routes/Models/ClimateZone.swift
Normal file
31
Sources/Routes/Models/ClimateZone.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
public enum ClimateZone: String, Codable, Equatable, Sendable {
|
||||||
|
case dry
|
||||||
|
case hotHumid
|
||||||
|
case marine
|
||||||
|
case moist
|
||||||
|
|
||||||
|
public var zoneIdentifiers: [String] {
|
||||||
|
switch self {
|
||||||
|
case .dry:
|
||||||
|
return ["2B", "3B", "4B", "5B", "6B", "7B"]
|
||||||
|
case .hotHumid:
|
||||||
|
return ["1A", "2A"]
|
||||||
|
case .marine:
|
||||||
|
return ["3C", "4C"]
|
||||||
|
case .moist:
|
||||||
|
return ["3A", "4A", "5A", "6A", "7A"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var cfmPerTon: Int {
|
||||||
|
switch self {
|
||||||
|
case .dry: return 450
|
||||||
|
case .hotHumid: return 350
|
||||||
|
case .marine, .moist: return 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var label: String {
|
||||||
|
return "\(rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ",")))"
|
||||||
|
}
|
||||||
|
}
|
||||||
112
Sources/Routes/Models/FilterPressureDrop.swift
Normal file
112
Sources/Routes/Models/FilterPressureDrop.swift
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
public enum FilterPressureDrop {
|
||||||
|
|
||||||
|
public enum Request: Codable, Equatable, Sendable {
|
||||||
|
case basic(Basic)
|
||||||
|
case fanLaw(FanLaw)
|
||||||
|
|
||||||
|
public struct Basic: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
let systemSize: HVACSystemSize
|
||||||
|
let climateZone: ClimateZone
|
||||||
|
let filterType: FilterType
|
||||||
|
let filterWidth: Double
|
||||||
|
let filterHeight: Double
|
||||||
|
|
||||||
|
public init(
|
||||||
|
systemSize: HVACSystemSize,
|
||||||
|
climateZone: ClimateZone,
|
||||||
|
filterType: FilterPressureDrop.FilterType,
|
||||||
|
filterWidth: Double,
|
||||||
|
filterHeight: Double
|
||||||
|
) {
|
||||||
|
self.systemSize = systemSize
|
||||||
|
self.climateZone = climateZone
|
||||||
|
self.filterType = filterType
|
||||||
|
self.filterWidth = filterWidth
|
||||||
|
self.filterHeight = filterHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FanLaw: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
let filterWidth: Double
|
||||||
|
let filterHeight: Double
|
||||||
|
let filterDepth: Double
|
||||||
|
let ratedAirflow: Double
|
||||||
|
let ratedPressureDrop: Double
|
||||||
|
let designAirflow: Double
|
||||||
|
|
||||||
|
public init(
|
||||||
|
filterWidth: Double,
|
||||||
|
filterHeight: Double,
|
||||||
|
filterDepth: Double,
|
||||||
|
ratedAirflow: Double,
|
||||||
|
ratedPressureDrop: Double,
|
||||||
|
designAirflow: Double
|
||||||
|
) {
|
||||||
|
self.filterWidth = filterWidth
|
||||||
|
self.filterHeight = filterHeight
|
||||||
|
self.filterDepth = filterDepth
|
||||||
|
self.ratedAirflow = ratedAirflow
|
||||||
|
self.ratedPressureDrop = ratedPressureDrop
|
||||||
|
self.designAirflow = designAirflow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Result: Codable, Equatable, Sendable {
|
||||||
|
case basic(Basic)
|
||||||
|
case fanLaw(FanLaw)
|
||||||
|
|
||||||
|
public struct Basic: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
let filterArea: Double
|
||||||
|
let feetPerMinute: Double
|
||||||
|
let initialPressureDrop: Double
|
||||||
|
let maxPressureDrop: Double
|
||||||
|
|
||||||
|
public init(filterArea: Double, feetPerMinute: Double, initialPressureDrop: Double, maxPressureDrop: Double) {
|
||||||
|
self.filterArea = filterArea
|
||||||
|
self.feetPerMinute = feetPerMinute
|
||||||
|
self.initialPressureDrop = initialPressureDrop
|
||||||
|
self.maxPressureDrop = maxPressureDrop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FanLaw: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let predictedPressureDrop: Double
|
||||||
|
public let velocityRatio: Double
|
||||||
|
public let pressureRatio: Double
|
||||||
|
public let faceVelocity: Double
|
||||||
|
|
||||||
|
public init(
|
||||||
|
predictedPressureDrop: Double,
|
||||||
|
velocityRatio: Double,
|
||||||
|
pressureRatio: Double,
|
||||||
|
faceVelocity: Double
|
||||||
|
) {
|
||||||
|
self.predictedPressureDrop = predictedPressureDrop
|
||||||
|
self.velocityRatio = velocityRatio
|
||||||
|
self.pressureRatio = pressureRatio
|
||||||
|
self.faceVelocity = faceVelocity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FilterType: String, Codable, Equatable, Sendable {
|
||||||
|
case fiberglass
|
||||||
|
case pleatedBasic
|
||||||
|
case pleatedBetter
|
||||||
|
case pleatedBest
|
||||||
|
|
||||||
|
public var label: String {
|
||||||
|
switch self {
|
||||||
|
case .fiberglass: return "Fiberglass (MERV 1-4)"
|
||||||
|
case .pleatedBasic: return "Basic Pleated (MERV 5-8)"
|
||||||
|
case .pleatedBetter: return "Better Pleated (MERV 9-12)"
|
||||||
|
case .pleatedBest: return "Best Pleated (MERV 13-16)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
Sources/Routes/Models/RoomPressure.swift
Normal file
126
Sources/Routes/Models/RoomPressure.swift
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
public enum RoomPressure {
|
||||||
|
|
||||||
|
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||||
|
case knownAirflow
|
||||||
|
case measuredPressure
|
||||||
|
|
||||||
|
public var label: String {
|
||||||
|
switch self {
|
||||||
|
case .knownAirflow: return "Known Airflow"
|
||||||
|
case .measuredPressure: return "Measured Pressure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Request: Codable, Equatable, Sendable {
|
||||||
|
case knownAirflow(KnownAirflow)
|
||||||
|
case measuredPressure(MeasuredPressure)
|
||||||
|
|
||||||
|
public struct KnownAirflow: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let targetRoomPressure: Double
|
||||||
|
public let doorWidth: Double
|
||||||
|
public let doorHeight: Double
|
||||||
|
public let doorUndercut: Double
|
||||||
|
public let supplyAirflow: Double
|
||||||
|
public let preferredGrilleHeight: CommonReturnGrilleHeight
|
||||||
|
|
||||||
|
public init(
|
||||||
|
targetRoomPressure: Double,
|
||||||
|
doorWidth: Double,
|
||||||
|
doorHeight: Double,
|
||||||
|
doorUndercut: Double,
|
||||||
|
supplyAirflow: Double,
|
||||||
|
preferredGrilleHeight: RoomPressure.CommonReturnGrilleHeight
|
||||||
|
) {
|
||||||
|
self.targetRoomPressure = targetRoomPressure
|
||||||
|
self.doorWidth = doorWidth
|
||||||
|
self.doorHeight = doorHeight
|
||||||
|
self.doorUndercut = doorUndercut
|
||||||
|
self.supplyAirflow = supplyAirflow
|
||||||
|
self.preferredGrilleHeight = preferredGrilleHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct MeasuredPressure: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let measuredRoomPressure: Double // pascals.
|
||||||
|
public let doorWidth: Double
|
||||||
|
public let doorHeight: Double
|
||||||
|
public let doorUndercut: Double
|
||||||
|
public let preferredGrilleHeight: CommonReturnGrilleHeight
|
||||||
|
|
||||||
|
public init(
|
||||||
|
measuredRoomPressure: Double,
|
||||||
|
doorWidth: Double,
|
||||||
|
doorHeight: Double,
|
||||||
|
doorUndercut: Double,
|
||||||
|
preferredGrilleHeight: RoomPressure.CommonReturnGrilleHeight
|
||||||
|
) {
|
||||||
|
self.measuredRoomPressure = measuredRoomPressure
|
||||||
|
self.doorWidth = doorWidth
|
||||||
|
self.doorHeight = doorHeight
|
||||||
|
self.doorUndercut = doorUndercut
|
||||||
|
self.preferredGrilleHeight = preferredGrilleHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Response: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let grilleSize: GrilleSize
|
||||||
|
public let ductSize: DuctSize
|
||||||
|
public let calculatedAirflow: Double?
|
||||||
|
public let warnings: [String]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
grilleSize: RoomPressure.Response.GrilleSize,
|
||||||
|
ductSize: RoomPressure.Response.DuctSize,
|
||||||
|
calculatedAirflow: Double? = nil,
|
||||||
|
warnings: [String]
|
||||||
|
) {
|
||||||
|
self.grilleSize = grilleSize
|
||||||
|
self.ductSize = ductSize
|
||||||
|
self.calculatedAirflow = calculatedAirflow
|
||||||
|
self.warnings = warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DuctSize: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let diameter: Double
|
||||||
|
public let velocity: Double
|
||||||
|
|
||||||
|
public init(diameter: Double, velocity: Double) {
|
||||||
|
self.diameter = diameter
|
||||||
|
self.velocity = velocity
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct GrilleSize: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let width: Double
|
||||||
|
public let height: Double
|
||||||
|
public let area: Double
|
||||||
|
|
||||||
|
public init(width: Double, height: Double, area: Double) {
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.area = area
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CommonReturnGrilleHeight: Int, CaseIterable, Codable, Equatable, Sendable {
|
||||||
|
case four = 4
|
||||||
|
case six = 6
|
||||||
|
case eight = 8
|
||||||
|
case ten = 10
|
||||||
|
case twelve = 12
|
||||||
|
case fourteen = 14
|
||||||
|
case twenty = 20
|
||||||
|
|
||||||
|
public var label: String { "\(rawValue)\"" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ public extension SiteRoute {
|
|||||||
case dehumidifierSize(DehumidifierSize)
|
case dehumidifierSize(DehumidifierSize)
|
||||||
case hvacSystemPerformance(HVACSystemPerformance)
|
case hvacSystemPerformance(HVACSystemPerformance)
|
||||||
case moldRisk(MoldRisk)
|
case moldRisk(MoldRisk)
|
||||||
|
case roomPressure(RoomPressure)
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
Route(.case(Self.index)) {
|
Route(.case(Self.index)) {
|
||||||
@@ -70,6 +71,9 @@ public extension SiteRoute {
|
|||||||
Route(.case(Self.moldRisk)) {
|
Route(.case(Self.moldRisk)) {
|
||||||
MoldRisk.router
|
MoldRisk.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.roomPressure)) {
|
||||||
|
RoomPressure.router
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DehumidifierSize: Equatable, Sendable {
|
public enum DehumidifierSize: Equatable, Sendable {
|
||||||
@@ -157,5 +161,23 @@ public extension SiteRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum RoomPressure: Equatable, Sendable {
|
||||||
|
case index(mode: Routes.RoomPressure.Mode? = nil)
|
||||||
|
|
||||||
|
public static var index: Self { .index() }
|
||||||
|
|
||||||
|
static let rootPath = "room-pressure"
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.index)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.get
|
||||||
|
Query {
|
||||||
|
Optionally { Field("form") { Routes.RoomPressure.Mode.parser() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,41 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
|
|
||||||
|
public struct PrimaryButton: HTML, Sendable {
|
||||||
|
let label: String
|
||||||
|
|
||||||
|
public init(label: String) {
|
||||||
|
self.label = label
|
||||||
|
}
|
||||||
|
|
||||||
|
public var content: some HTML<HTMLTag.button> {
|
||||||
|
button(
|
||||||
|
.class("""
|
||||||
|
font-bold py-2 px-4 transition-colors
|
||||||
|
bg-blue-500 enabled:hover:bg-blue-600
|
||||||
|
text-yellow-300
|
||||||
|
""")
|
||||||
|
) { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SecondaryButton: HTML, Sendable {
|
||||||
|
let label: String
|
||||||
|
|
||||||
|
public init(label: String) {
|
||||||
|
self.label = label
|
||||||
|
}
|
||||||
|
|
||||||
|
public var content: some HTML<HTMLTag.button> {
|
||||||
|
button(
|
||||||
|
.class("""
|
||||||
|
font-bold py-2 px-4 transition-colors
|
||||||
|
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||||
|
text-blue-600
|
||||||
|
""")
|
||||||
|
) { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct SubmitButton: HTML, Sendable {
|
public struct SubmitButton: HTML, Sendable {
|
||||||
let label: String
|
let label: String
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import Elementary
|
|||||||
|
|
||||||
extension HTMLAttribute where Tag == HTMLTag.input {
|
extension HTMLAttribute where Tag == HTMLTag.input {
|
||||||
|
|
||||||
|
static func max(_ value: String) -> Self {
|
||||||
|
.init(name: "max", value: value)
|
||||||
|
}
|
||||||
|
|
||||||
static func min(_ value: String) -> Self {
|
static func min(_ value: String) -> Self {
|
||||||
.init(name: "min", value: value)
|
.init(name: "min", value: value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ extension ViewController: DependencyKey {
|
|||||||
case let .hvacSystemPerformance(route):
|
case let .hvacSystemPerformance(route):
|
||||||
switch route {
|
switch route {
|
||||||
case .index:
|
case .index:
|
||||||
return request.respond(HVACSystemPerformanceForm())
|
// let response = try await HVACSystemPerformance.Response.mock()
|
||||||
|
return request.respond(HVACSystemPerformanceForm(response: nil))
|
||||||
case let .submit(hvacRequest):
|
case let .submit(hvacRequest):
|
||||||
let response = try await hvacRequest.respond(logger: request.logger)
|
let response = try await hvacRequest.respond(logger: request.logger)
|
||||||
return HVACSystemPerformanceResult(response: response)
|
return HVACSystemPerformanceResult(response: response)
|
||||||
@@ -95,6 +96,12 @@ extension ViewController: DependencyKey {
|
|||||||
let response = try await psychrometricClient.respond(request)
|
let response = try await psychrometricClient.respond(request)
|
||||||
return MoldRiskResponse(response: response)
|
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: "%")
|
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"
|
"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.
|
// Display psychrometric properties.
|
||||||
PsychrometricPropertiesGrid(properties: response.psychrometricProperties)
|
Properties(properties: response.psychrometricProperties)
|
||||||
.attributes(.class("mx-6"))
|
.attributes(.class("mt-8"))
|
||||||
|
|
||||||
// Disclaimer.
|
// Disclaimer.
|
||||||
Note {
|
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 {
|
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
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
x-shared_environment: &shared_environment
|
x-shared_environment: &shared_environment
|
||||||
LOG_LEVEL: ${LOG_LEVEL:-debug}
|
LOG_LEVEL: ${LOG_LEVEL:-debug}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: swift-hvac-toolbox:latest
|
image: swift-hvac-toolbox:latest
|
||||||
@@ -24,6 +24,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
<<: *shared_environment
|
<<: *shared_environment
|
||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8081:8080'
|
||||||
# user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user.
|
# user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user.
|
||||||
command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
||||||
|
|||||||
Reference in New Issue
Block a user