Files
swift-hvac-toolbox/Sources/ApiController/Extensions/RoomPressure.swift

223 lines
7.4 KiB
Swift

import Foundation
import Logging
import OrderedCollections
import Routes
public extension RoomPressure.Request {
static let pascalToIWCMultiplier = 0.004014
static let targetVelocity = 400.0
static let maxVelocity = 600.0
static let minGrilleFreeArea = 0.7
static let standardDuctSizes = OrderedSet([4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20])
static let standardGrilleHeights = OrderedSet([4, 6, 8, 10, 12, 14, 20])
static let standardGrilleSizes = [
4: OrderedSet([8, 10, 12, 14]),
6: OrderedSet([8, 10, 12, 14]),
8: OrderedSet([10, 12, 14]),
10: OrderedSet([10]),
12: OrderedSet([12]),
14: OrderedSet([14])
]
// OrderedSet([
// (width: 8, height: 4),
// (width: 10, height: 4),
// (width: 12, height: 4),
// (width: 14, height: 4),
// (width: 8, height: 6),
// (width: 10, height: 6),
// (width: 12, height: 6),
// (width: 14, height: 6),
// (width: 10, height: 8),
// (width: 12, height: 8),
// (width: 14, height: 8),
// (width: 10, height: 10),
// (width: 12, height: 12),
// (width: 14, height: 14)
// ])
func respond(logger: Logger) async throws -> RoomPressure.Response {
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 {
Self.standardDuctSizes.sorted()
.first { $0 >= calculatedSize }
?? 20
}
private func getStandardGrilleSize(requiredArea: Double, selectedHeight: Int) throws -> (width: Int, height: Int) {
guard let availableSizes = Self.standardGrilleSizes[selectedHeight] else {
throw RoomPressureError.invalidPreferredHeight
}
if let width = availableSizes.first(where: {
Double($0) * Double(selectedHeight) * Self.minGrilleFreeArea >= (requiredArea / Self.minGrilleFreeArea)
}) {
return (width, selectedHeight)
}
// If no width matches return largest for the selectedHeight.
if let largestSize = availableSizes.last { return (largestSize, selectedHeight) }
return (14, selectedHeight)
}
// Calculate door leakage area in sq. ft.
private func calculateDoorLeakageArea(doorWidth: Double, doorHeight: Double, doorUndercut: Double) -> Double {
let doorLeakageArea = (doorWidth * doorUndercut) / 144.0
let doorPerimiterLeakage = ((2 * doorHeight + doorWidth) * 0.125) / 144.0
return doorLeakageArea + doorPerimiterLeakage
}
enum RoomPressureError: Error {
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.")
}
}
}