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

159 lines
4.5 KiB
Swift

import CoreModels
import Foundation
import Logging
import Routes
public extension HeatingBalancePoint.Request {
func respond(logger: Logger) async throws -> HeatingBalancePoint.Response {
switch self {
case let .thermal(request):
logger.debug("Calculating thermal balance point: \(request)")
return try await request.respond(logger: logger)
}
}
}
private extension HeatingBalancePoint.Request.Thermal {
static let heatPumpDeratingFactor = 0.014
func respond(logger: Logger) async throws -> HeatingBalancePoint.Response {
try validate()
let (at47, at17) = getCapacities()
let heatingDesignTemperature = getHeatingDesignTemperature()
let buildingHeatLoss = getBuildingHeatLoss(designTemperature: heatingDesignTemperature)
let balancePoint = await thermalBalancePoint(
heatLoss: buildingHeatLoss,
at47: at47,
at17: at17,
designTemperature: heatingDesignTemperature
)
let warnings = getWarnings()
return .thermal(.init(
capacityAt47: at47,
capacityAt17: at17,
balancePointTemperature: balancePoint,
heatLoss: buildingHeatLoss,
heatLossMode: self.buildingHeatLoss.mode,
heatingDesignTemperature: heatingDesignTemperature,
warnings: warnings
))
}
func getWarnings() -> [String] {
var warnings = [String]()
if capacityAt17 == nil || capacityAt47 == nil {
warnings.append(
"Heat pump capacities are estimated based on system size - include actual capacities for higher accuracy."
)
}
if case .estimated = buildingHeatLoss {
warnings.append(
"Building heat loss is estimated based on climate zone - include actual heat loss for higher accuracy"
)
}
if heatingDesignTemperature == nil {
warnings.append(
"""
Heating outdoor design temperature is based on average for climate zone - include outdoor design temperature
for higher accuracy.
"""
)
}
return warnings
}
func getBuildingHeatLoss(designTemperature: Double) -> Double {
switch buildingHeatLoss {
case let .known(btu: btu): return btu
case let .estimated(squareFeet: squareFeet):
return squareFeet * climateZone!.averageHeatLossPerSquareFoot * (70 - designTemperature)
}
}
func getCapacities() -> (at47: Double, at17: Double) {
let at47 = capacityAt47 ?? systemSize * 12000
let at17 = capacityAt17 ?? at47 * (1 - Self.heatPumpDeratingFactor * (47 - 17))
return (at47, at17)
}
func getHeatingDesignTemperature() -> Double {
guard let heatingDesignTemperature else {
return climateZone!.averageHeatingDesignTemperature
}
return heatingDesignTemperature
}
func validate() throws {
guard systemSize > 0 else {
throw ValidationError(message: "System size should be greater than 0.")
}
switch buildingHeatLoss {
case let .known(btu: btu):
guard btu > 0 else {
throw ValidationError(message: "Building heat loss btu's should be greater than 0.")
}
case let .estimated(squareFeet: squareFeet):
guard squareFeet > 0 else {
throw ValidationError(message: "Building squareFeet should be greater than 0.")
}
guard climateZone != nil else {
throw ValidationError(message: "Climate zone is required when estimating heat loss.")
}
}
if let capacityAt47 {
guard capacityAt47 > 0 else {
throw ValidationError(message: "Heat pump capacity @ 47 should be greater than 0.")
}
}
if let capacityAt17 {
guard capacityAt17 > 0 else {
throw ValidationError(message: "Heat pump capacity @ 17 should be greater than 0.")
}
}
}
}
extension ClimateZone {
var averageHeatLossPerSquareFoot: Double {
switch self {
case .one: return 0.08
case .two: return 0.1
case .three: return 0.125
case .four: return 0.15
case .five: return 0.19
case .six: return 0.25
case .seven: return 0.3
}
}
var averageHeatingDesignTemperature: Double {
switch self {
case .one: return 45
case .two: return 35
case .three: return 27
case .four: return 17
case .five: return 7
case .six: return -5
case .seven: return -15
}
}
}
private func thermalBalancePoint(
heatLoss: Double,
at47: Double,
at17: Double,
designTemperature: Double
) async -> Double {
(30.0 * (((designTemperature - 65.0) * at47) + (65.0 * heatLoss))
- ((designTemperature - 65.0) * (at47 - at17) * 47.0))
/ ((30.0 * heatLoss) - ((designTemperature - 65.0) * (at47 - at17)))
}