From af24ef3971ebf141ea098eb7dfd8233699c5fde7 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 28 Feb 2025 22:45:59 -0500 Subject: [PATCH] feat: Begins room pressure calculations --- Sources/ApiController/ApiController.swift | 7 ++ .../Extensions/RoomPressure.swift | 76 +++++++++++++++++++ Sources/Routes/Models/RoomPressure.swift | 2 +- Sources/Routes/SiteRoutes.swift | 44 +++++++++++ Sources/ViewController/Live.swift | 4 + justfile | 5 -- 6 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 Sources/ApiController/Extensions/RoomPressure.swift diff --git a/Sources/ApiController/ApiController.swift b/Sources/ApiController/ApiController.swift index 2b96537..a295132 100644 --- a/Sources/ApiController/ApiController.swift +++ b/Sources/ApiController/ApiController.swift @@ -25,6 +25,8 @@ extension ApiController: DependencyKey { @Dependency(\.psychrometricClient) var psychrometricClient return .init(json: { route, logger in + logger.debug("API Route: \(route)") + switch route { case let .calculateDehumidifierSize(request): logger.debug("Calculating dehumidifier size: \(request)") @@ -37,6 +39,11 @@ extension ApiController: DependencyKey { case let .calculateMoldRisk(request): logger.debug("Calculating mold risk: \(request)") return try await psychrometricClient.respond(request, logger) + + case let .calculateRoomPressure(request): + logger.debug("Calculating room pressure: \(request)") + // FIX: + fatalError() } }) } diff --git a/Sources/ApiController/Extensions/RoomPressure.swift b/Sources/ApiController/Extensions/RoomPressure.swift new file mode 100644 index 0000000..136296c --- /dev/null +++ b/Sources/ApiController/Extensions/RoomPressure.swift @@ -0,0 +1,76 @@ +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 { + fatalError() + } + + 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 + } +} diff --git a/Sources/Routes/Models/RoomPressure.swift b/Sources/Routes/Models/RoomPressure.swift index de451d2..9788832 100644 --- a/Sources/Routes/Models/RoomPressure.swift +++ b/Sources/Routes/Models/RoomPressure.swift @@ -119,7 +119,7 @@ public enum RoomPressure { case ten = 10 case twelve = 12 case fourteen = 14 - case twenty = 20 + // case twenty = 20 public var label: String { "\(rawValue)\"" } } diff --git a/Sources/Routes/SiteRoutes.swift b/Sources/Routes/SiteRoutes.swift index 6174624..1fd4985 100644 --- a/Sources/Routes/SiteRoutes.swift +++ b/Sources/Routes/SiteRoutes.swift @@ -26,6 +26,7 @@ public extension SiteRoute { case calculateDehumidifierSize(DehumidifierSize.Request) case calculateHVACSystemPerformance(HVACSystemPerformance.Request) case calculateMoldRisk(MoldRisk.Request) + case calculateRoomPressure(RoomPressure.Request) static let rootPath = Path { "api"; "v1" } @@ -45,6 +46,16 @@ public extension SiteRoute { Method.post Body(.json(MoldRisk.Request.self)) } + Route(.case(Self.calculateRoomPressure)) { + Path { "api"; "v1"; "calculateRoomPressure" } + Method.post + OneOf { + Body(.json(RoomPressure.Request.KnownAirflow.self)) + .map(.case(RoomPressure.Request.knownAirflow)) + Body(.json(RoomPressure.Request.MeasuredPressure.self)) + .map(.case(RoomPressure.Request.measuredPressure)) + } + } } } } @@ -164,6 +175,7 @@ public extension SiteRoute { public enum RoomPressure: Equatable, Sendable { case index(mode: Routes.RoomPressure.Mode? = nil) + case submit(Routes.RoomPressure.Request) public static var index: Self { .index() } @@ -177,6 +189,38 @@ public extension SiteRoute { Optionally { Field("form") { Routes.RoomPressure.Mode.parser() } } } } + Route(.case(Self.submit)) { + Path { rootPath } + Method.post + Body { + OneOf { + FormData { + Field("targetRoomPressure") { Double.parser() } + Field("doorWidth") { Double.parser() } + Field("doorHeight") { Double.parser() } + Field("doorUndercut") { Double.parser() } + Field("supplyAirflow") { Double.parser() } + Field("preferredGrilleHeight") { + Routes.RoomPressure.CommonReturnGrilleHeight.parser() + } + } + .map(.memberwise(Routes.RoomPressure.Request.KnownAirflow.init)) + .map(.case(Routes.RoomPressure.Request.knownAirflow)) + + FormData { + Field("measuredRoomPressure") { Double.parser() } + Field("doorWidth") { Double.parser() } + Field("doorHeight") { Double.parser() } + Field("doorUndercut") { Double.parser() } + Field("preferredGrilleHeight") { + Routes.RoomPressure.CommonReturnGrilleHeight.parser() + } + } + .map(.memberwise(Routes.RoomPressure.Request.MeasuredPressure.init)) + .map(.case(Routes.RoomPressure.Request.measuredPressure)) + } + } + } } } } diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index bf3385d..5442949 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -107,6 +107,10 @@ extension ViewController: DependencyKey { switch route { case let .index(mode): return request.respond(RoomPressureForm(mode: mode, response: nil)) + + // FIX: + case .submit: + fatalError() } } }) diff --git a/justfile b/justfile index 8c79868..f44b1f2 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,5 @@ docker_image := "hvac-toolbox" docker_tag := "latest" -do_registery := "registry.digitalocean.com/swift-hvac-toolbox" build-docker: @docker build -t {{docker_image}}:{{docker_tag}} . @@ -17,7 +16,3 @@ run-css: clean: @rm -rf .build - -push-image tag="prod": - @docker tag {{docker_image}}:{{docker_tag}} {{do_registery}}/{{docker_image}}:{{tag}} - @docker push {{do_registery}}/{{docker_image}}:{{tag}}