feat: Adds furnace heating interpolations.
All checks were successful
CI / Ubuntu (push) Successful in 11m23s
All checks were successful
CI / Ubuntu (push) Successful in 11m23s
This commit is contained in:
9
Sources/ManualS/Extensions/NormalizePercentage.swift
Normal file
9
Sources/ManualS/Extensions/NormalizePercentage.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import CoreFoundation
|
||||
|
||||
func normalizePercentage(
|
||||
_ lhs: Int,
|
||||
_ rhs: Int
|
||||
) -> Int {
|
||||
let value = Double(lhs) / Double(rhs)
|
||||
return Int((value * 1000).rounded() / 10.0)
|
||||
}
|
||||
@@ -129,14 +129,6 @@ private extension SizingLimits.Response {
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizePercentage(
|
||||
_ lhs: Int,
|
||||
_ rhs: Int
|
||||
) -> Int {
|
||||
let value = Double(lhs) / Double(rhs)
|
||||
return Int((value * 1000).rounded() / 10.0)
|
||||
}
|
||||
|
||||
private extension CoolingInterpolation.InterpolationRequest {
|
||||
|
||||
func interpolatedCapacity(outdoorDesignTemperature: Int) async -> Capacity.Cooling {
|
||||
|
||||
55
Sources/ManualS/Internal/FurnaceInterpolation.swift
Normal file
55
Sources/ManualS/Internal/FurnaceInterpolation.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Models
|
||||
import Validations
|
||||
|
||||
extension FurnaceInterpolation.Request {
|
||||
private static let undersizingLimit = 90
|
||||
private static let oversizingLimit = 140
|
||||
|
||||
func respond() async throws -> FurnaceInterpolation.Response {
|
||||
try await validate()
|
||||
let altitudeDerating = try await altitudeDerating()
|
||||
let outputCapacity = Int(
|
||||
(Double(inputRating) * (altitudeDerating ?? 1.0))
|
||||
* (afue / 100)
|
||||
)
|
||||
let capacityAsPercentOfLoad = normalizePercentage(outputCapacity, heatLoss)
|
||||
|
||||
var failures = [String]()
|
||||
if capacityAsPercentOfLoad < Self.undersizingLimit {
|
||||
failures.append("Undersizing failure.")
|
||||
}
|
||||
if capacityAsPercentOfLoad > Self.oversizingLimit {
|
||||
failures.append("Oversizing failure.")
|
||||
}
|
||||
|
||||
return .init(
|
||||
failures: failures.isEmpty ? nil : failures,
|
||||
altitudeAdjustmentMultiplier: altitudeDerating,
|
||||
capacityAsPercentOfLoad: capacityAsPercentOfLoad,
|
||||
oversizingLimit: Self.oversizingLimit,
|
||||
undersizingLimit: Self.undersizingLimit,
|
||||
outputCapacity: outputCapacity
|
||||
)
|
||||
}
|
||||
|
||||
func altitudeDerating() async throws -> Double? {
|
||||
guard let elevation else { return nil }
|
||||
let response = try await Derating.Request(
|
||||
elevation: elevation,
|
||||
systemType: .heatingOnly(type: .furnace)
|
||||
).respond()
|
||||
return response.heating
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FurnaceInterpolation.Request: AsyncValidatable {
|
||||
public var body: some AsyncValidation<Self> {
|
||||
AsyncValidator.accumulating {
|
||||
AsyncValidator.greaterThan(\.heatLoss, 0)
|
||||
AsyncValidator.greaterThan(\.inputRating, 0)
|
||||
AsyncValidator.greaterThan(\.afue, 0)
|
||||
AsyncValidator.lessThan(\.afue, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,20 @@ public extension DependencyValues {
|
||||
@DependencyClient
|
||||
public struct ManualS: Sendable {
|
||||
public var balancePoint: @Sendable (BalancePoint.Request) async throws -> BalancePoint.Response
|
||||
|
||||
public var derating: @Sendable (Derating.Request) async throws -> Derating.Response
|
||||
|
||||
public var coolingInterpolation:
|
||||
@Sendable (CoolingInterpolation.Request) async throws -> CoolingInterpolation.Response
|
||||
|
||||
public var furnaceInterpolation:
|
||||
@Sendable (FurnaceInterpolation.Request) async throws -> FurnaceInterpolation.Response
|
||||
|
||||
public var heatPumpHeatingInterpolation:
|
||||
@Sendable (HeatPumpHeatingInterpolation.Request) async throws -> HeatPumpHeatingInterpolation.Response
|
||||
|
||||
public var requiredKW: @Sendable (RequiredKW.Request) async throws -> RequiredKW.Response
|
||||
|
||||
public var sizingLimits: @Sendable (SizingLimits.Request) async throws -> SizingLimits.Response
|
||||
}
|
||||
|
||||
@@ -27,6 +34,7 @@ extension ManualS: DependencyKey {
|
||||
balancePoint: { try await $0.respond() },
|
||||
derating: { try await $0.respond() },
|
||||
coolingInterpolation: { try await $0.respond() },
|
||||
furnaceInterpolation: { try await $0.respond() },
|
||||
heatPumpHeatingInterpolation: { try await $0.respond() },
|
||||
requiredKW: { try await $0.respond() },
|
||||
sizingLimits: { try await $0.respond() }
|
||||
|
||||
53
Sources/Models/FurnaceInterpolation.swift
Normal file
53
Sources/Models/FurnaceInterpolation.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
public enum FurnaceInterpolation {
|
||||
|
||||
public struct Request: Codable, Equatable, Sendable {
|
||||
|
||||
public let elevation: Int?
|
||||
public let winterDesignTemperature: Int
|
||||
public let heatLoss: Int
|
||||
public let inputRating: Int
|
||||
public let afue: Double
|
||||
|
||||
public init(
|
||||
elevation: Int? = nil,
|
||||
winterDesignTemperature: Int,
|
||||
heatLoss: Int,
|
||||
inputRating: Int,
|
||||
afue: Double
|
||||
) {
|
||||
self.elevation = elevation
|
||||
self.winterDesignTemperature = winterDesignTemperature
|
||||
self.heatLoss = heatLoss
|
||||
self.inputRating = inputRating
|
||||
self.afue = afue
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable, Equatable, Sendable {
|
||||
|
||||
public let failed: Bool
|
||||
public let failures: [String]?
|
||||
public let altitudeAdjustmentMultiplier: Double?
|
||||
public let capacityAsPercentOfLoad: Int
|
||||
public let oversizingLimit: Int
|
||||
public let undersizingLimit: Int
|
||||
public let outputCapacity: Int
|
||||
|
||||
public init(
|
||||
failures: [String]? = nil,
|
||||
altitudeAdjustmentMultiplier: Double? = nil,
|
||||
capacityAsPercentOfLoad: Int,
|
||||
oversizingLimit: Int,
|
||||
undersizingLimit: Int,
|
||||
outputCapacity: Int
|
||||
) {
|
||||
self.failed = failures == nil ? false : !failures!.isEmpty
|
||||
self.failures = failures
|
||||
self.altitudeAdjustmentMultiplier = altitudeAdjustmentMultiplier
|
||||
self.capacityAsPercentOfLoad = capacityAsPercentOfLoad
|
||||
self.oversizingLimit = oversizingLimit
|
||||
self.undersizingLimit = undersizingLimit
|
||||
self.outputCapacity = outputCapacity
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,51 @@ struct ManualSTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func furnaceInterpolation() async throws {
|
||||
try await withDependencies {
|
||||
$0.manualS = .liveValue
|
||||
} operation: {
|
||||
@Dependency(\.manualS) var manualS
|
||||
|
||||
let response = try await manualS.furnaceInterpolation(
|
||||
.init(
|
||||
elevation: nil,
|
||||
winterDesignTemperature: 5,
|
||||
heatLoss: 49667,
|
||||
inputRating: 60000,
|
||||
afue: 96
|
||||
)
|
||||
)
|
||||
|
||||
#expect(response.failed == false)
|
||||
#expect(response.capacityAsPercentOfLoad == 116)
|
||||
#expect(response.outputCapacity == 57600)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func furnaceInterpolation_with_oversizedFurnace() async throws {
|
||||
try await withDependencies {
|
||||
$0.manualS = .liveValue
|
||||
} operation: {
|
||||
@Dependency(\.manualS) var manualS
|
||||
|
||||
let response = try await manualS.furnaceInterpolation(
|
||||
.init(
|
||||
elevation: nil,
|
||||
winterDesignTemperature: 5,
|
||||
heatLoss: 49667,
|
||||
inputRating: 160_000,
|
||||
afue: 96
|
||||
)
|
||||
)
|
||||
|
||||
#expect(response.failed)
|
||||
#expect(response.failures == ["Oversizing failure."])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func balancePoint() async throws {
|
||||
try await withDependencies {
|
||||
|
||||
Reference in New Issue
Block a user