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))) }