diff --git a/Sources/ApiController/Extensions/RoomPressure.swift b/Sources/ApiController/Extensions/RoomPressure.swift index 136296c..3e67a2d 100644 --- a/Sources/ApiController/Extensions/RoomPressure.swift +++ b/Sources/ApiController/Extensions/RoomPressure.swift @@ -38,7 +38,116 @@ public extension RoomPressure.Request { // ]) func respond(logger: Logger) async throws -> RoomPressure.Response { - fatalError() + switch self { + case let .knownAirflow(request): + return try await calculateKnownAirflow(request, logger) + case let .measuredPressure(request): + return try await calculateMeasuredPressure(request, logger) + } + } + + private func calculateKnownAirflow( + _ request: RoomPressure.Request.KnownAirflow, + _ logger: Logger + ) async throws -> RoomPressure.Response { + try request.validate() + + let totalLeakageArea = calculateDoorLeakageArea( + doorWidth: request.doorWidth, + doorHeight: request.doorHeight, + doorUndercut: request.doorUndercut + ) + + // Net free area (in^2) + let netFreeArea = (request.supplyAirflow / Self.targetVelocity) * 144.0 + let grilleDimensions = try getStandardGrilleSize( + requiredArea: netFreeArea, + selectedHeight: request.preferredGrilleHeight.rawValue + ) + let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: request.supplyAirflow) + + return .init( + grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144), + ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity), + warnings: generateWarnings( + roomPressure: request.targetRoomPressure, + actualVelocity: actualVelocity, + totalLeakageArea: totalLeakageArea, + netFreeArea: netFreeArea + ) + ) + } + + private func calculateMeasuredPressure( + _ request: RoomPressure.Request.MeasuredPressure, + _ logger: Logger + ) async throws -> RoomPressure.Response { + try request.validate() + + let totalLeakageArea = calculateDoorLeakageArea( + doorWidth: request.doorWidth, + doorHeight: request.doorHeight, + doorUndercut: request.doorUndercut + ) + + let pressureInchesWaterColumn = request.measuredRoomPressure * Self.pascalToIWCMultiplier + let calculatedAirflow = totalLeakageArea * 4005 * sqrt(pressureInchesWaterColumn) + // in^2 + let netFreeArea = (calculatedAirflow / Self.targetVelocity) * 144 + let grilleDimensions = try getStandardGrilleSize( + requiredArea: netFreeArea, + selectedHeight: request.preferredGrilleHeight.rawValue + ) + let (standardDuctSize, actualVelocity) = calculateDuctMetrics(for: calculatedAirflow) + + return .init( + grilleSize: .init(width: grilleDimensions.width, height: grilleDimensions.height, area: netFreeArea / 144), + ductSize: .init(diameter: standardDuctSize, velocity: actualVelocity), + calculatedAirflow: calculatedAirflow, + warnings: generateWarnings( + roomPressure: request.measuredRoomPressure, + actualVelocity: actualVelocity, + totalLeakageArea: totalLeakageArea, + netFreeArea: netFreeArea + ) + ) + } + + private func calculateDuctMetrics(for airflow: Double) -> (diameter: Int, velocity: Double) { + let ductArea = airflow / Self.targetVelocity + let ductDiameter = sqrt((4 * ductArea) / Double.pi) * 12 + let standardDuctSize = getStandardDuctSize(for: Int(ductDiameter)) + let actualVelocity = airflow / (Double.pi * pow(Double(standardDuctSize) / 24, 2)) + + return (standardDuctSize, actualVelocity) + } + + private func generateWarnings( + roomPressure: Double, + actualVelocity: Double, + totalLeakageArea: Double, + netFreeArea: Double + ) -> [String] { + var warnings = [String]() + + if roomPressure > 3 { + warnings.append( + "Room pressure exceeds 3 Pascals - consider reducing pressure differntial." + ) + } + + if totalLeakageArea > netFreeArea / 144 * 0.5 { + warnings.append( + "Door leakage area is significant - consider reducing gaps or increasing grille size." + ) + } + + if actualVelocity > Self.maxVelocity { + warnings.append( + "Return air velocity exceeds maximum recommended velocity - consider next size up duct." + ) + } + return [] } private func getStandardDuctSize(for calculatedSize: Int) -> Int { @@ -74,3 +183,40 @@ public extension RoomPressure.Request { case invalidPreferredHeight } } + +extension RoomPressure.Request.KnownAirflow { + func validate() throws { + guard targetRoomPressure > 0 else { + throw ValidationError(message: "Target room pressure should be greater than 0.") + } + guard doorWidth > 0 else { + throw ValidationError(message: "Door width should be greater than 0.") + } + guard doorHeight > 0 else { + throw ValidationError(message: "Door height should be greater than 0.") + } + guard doorUndercut > 0 else { + throw ValidationError(message: "Door undercut should be greater than 0.") + } + guard supplyAirflow > 0 else { + throw ValidationError(message: "Supply airflow should be greater than 0.") + } + } +} + +extension RoomPressure.Request.MeasuredPressure { + func validate() throws { + guard measuredRoomPressure > 0 else { + throw ValidationError(message: "MeasuredPressure room pressure should be greater than 0.") + } + guard doorWidth > 0 else { + throw ValidationError(message: "Door width should be greater than 0.") + } + guard doorHeight > 0 else { + throw ValidationError(message: "Door height should be greater than 0.") + } + guard doorUndercut > 0 else { + throw ValidationError(message: "Door undercut should be greater than 0.") + } + } +} diff --git a/Sources/ApiController/ValidationError.swift b/Sources/ApiController/ValidationError.swift new file mode 100644 index 0000000..7a2bc77 --- /dev/null +++ b/Sources/ApiController/ValidationError.swift @@ -0,0 +1,3 @@ +struct ValidationError: Error { + let message: String +} diff --git a/Sources/Routes/Models/RoomPressure.swift b/Sources/Routes/Models/RoomPressure.swift index 9788832..6d7fee2 100644 --- a/Sources/Routes/Models/RoomPressure.swift +++ b/Sources/Routes/Models/RoomPressure.swift @@ -87,10 +87,10 @@ public enum RoomPressure { public struct DuctSize: Codable, Equatable, Sendable { - public let diameter: Double + public let diameter: Int public let velocity: Double - public init(diameter: Double, velocity: Double) { + public init(diameter: Int, velocity: Double) { self.diameter = diameter self.velocity = velocity } @@ -99,11 +99,11 @@ public enum RoomPressure { public struct GrilleSize: Codable, Equatable, Sendable { - public let width: Double - public let height: Double + public let width: Int + public let height: Int public let area: Double - public init(width: Double, height: Double, area: Double) { + public init(width: Int, height: Int, area: Double) { self.width = width self.height = height self.area = area