feat: Finishes economic balance point.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "af48ddc16f0ca460cff188345cf870056ca53edee23c016080fc1ad55f366bf9",
|
||||
"originHash" : "16a33bebe1b38c2194b6ad57854e74c156212cd21d65cd12ccf6df7a6b89886c",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "async-http-client",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
import CoreModels
|
||||
import Foundation
|
||||
import Logging
|
||||
import OpcostAnalysis
|
||||
import Routes
|
||||
|
||||
public extension HeatingBalancePoint.Request {
|
||||
@@ -10,6 +11,42 @@ public extension HeatingBalancePoint.Request {
|
||||
case let .thermal(request):
|
||||
logger.debug("Calculating thermal balance point: \(request)")
|
||||
return try await request.respond(logger: logger)
|
||||
case let .economic(request):
|
||||
logger.debug("Calculating economic balance point: \(request)")
|
||||
return try await request.respond(logger: logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension HeatingBalancePoint.Request.Economic {
|
||||
|
||||
func respond(logger: Logger) async throws -> HeatingBalancePoint.Response {
|
||||
try validate()
|
||||
let fuelCostPerMMBTU = fuelType.costPerMMBTU(afue: fuelAFUE, costPerUnit: fuelCostPerUnit)
|
||||
logger.debug("Fuel cost per mmBTU: \(fuelCostPerMMBTU)")
|
||||
let electricCostPerMMBTU = 1_000_000 / 3412 * costPerKW
|
||||
let electricFuelRatio = electricCostPerMMBTU / fuelCostPerMMBTU
|
||||
let balancePointTemperature = (electricFuelRatio - 1.8273) / 0.0413
|
||||
let copAtBalancePoint = 0.0413 * balancePointTemperature + 1.8273
|
||||
|
||||
return .economic(.init(
|
||||
balancePointTemperature: balancePointTemperature,
|
||||
fuelCostPerMMBTU: fuelCostPerMMBTU,
|
||||
electricCostPerMMBTU: electricCostPerMMBTU,
|
||||
copAtBalancePoint: copAtBalancePoint,
|
||||
electricFuelRatio: electricFuelRatio
|
||||
))
|
||||
}
|
||||
|
||||
func validate() throws {
|
||||
guard fuelCostPerUnit > 0 else {
|
||||
throw ValidationError(message: "Fuel cost should be greater than 0.")
|
||||
}
|
||||
guard fuelAFUE > 0, fuelAFUE < 100 else {
|
||||
throw ValidationError(message: "AFUE is outside of range 0-100%.")
|
||||
}
|
||||
guard costPerKW > 0 else {
|
||||
throw ValidationError(message: "Electric cost per KW should be greater than 0.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +181,35 @@ extension ClimateZone {
|
||||
case .seven: return -15
|
||||
}
|
||||
}
|
||||
|
||||
var averageHeatingLoadHours: Double {
|
||||
switch self {
|
||||
case .one: return 1000
|
||||
case .two: return 2250
|
||||
case .three: return 3750
|
||||
case .four: return 5250
|
||||
case .five: return 6750
|
||||
case .six: return 8250
|
||||
case .seven: return 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension HeatingBalancePoint.FuelType {
|
||||
var btuPerUnit: Double {
|
||||
switch self {
|
||||
case .naturalGas: return 100_000
|
||||
case .propane: return 91333
|
||||
case .oil: return 138_690
|
||||
}
|
||||
}
|
||||
|
||||
func costPerMMBTU(afue: Double, costPerUnit: Double) -> Double {
|
||||
if self == .naturalGas {
|
||||
return costPerUnit * 10 / afue * 100
|
||||
}
|
||||
return (1_000_000 / btuPerUnit) * costPerUnit / afue * 100
|
||||
}
|
||||
}
|
||||
|
||||
private func thermalBalancePoint(
|
||||
|
||||
@@ -4,13 +4,15 @@ import Vapor
|
||||
#if DEBUG
|
||||
struct BrowserSyncHandler: LifecycleHandler {
|
||||
func didBoot(_ application: Application) throws {
|
||||
let process = Process()
|
||||
process.executableURL = URL(filePath: "/bin/sh")
|
||||
process.arguments = ["-c", "browser-sync reload"]
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
print("Could not auto-reload: \(error)")
|
||||
if Environment.get("BROWSER_AUTO_RELOAD") != nil {
|
||||
let process = Process()
|
||||
process.executableURL = URL(filePath: "/bin/sh")
|
||||
process.arguments = ["-c", "browser-sync reload"]
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
print("Could not auto-reload: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,29 @@ public enum HeatingBalancePoint {
|
||||
}
|
||||
|
||||
public enum Request: Codable, Equatable, Sendable {
|
||||
case economic(Economic)
|
||||
case thermal(Thermal)
|
||||
|
||||
public struct Economic: Codable, Equatable, Sendable {
|
||||
|
||||
public let fuelType: FuelType
|
||||
public let fuelCostPerUnit: Double
|
||||
public let fuelAFUE: Double
|
||||
public let costPerKW: Double
|
||||
|
||||
public init(
|
||||
fuelType: HeatingBalancePoint.FuelType,
|
||||
fuelCostPerUnit: Double,
|
||||
fuelAFUE: Double,
|
||||
costPerKW: Double
|
||||
) {
|
||||
self.fuelType = fuelType
|
||||
self.fuelCostPerUnit = fuelCostPerUnit
|
||||
self.fuelAFUE = fuelAFUE
|
||||
self.costPerKW = costPerKW
|
||||
}
|
||||
}
|
||||
|
||||
public struct Thermal: Codable, Equatable, Sendable {
|
||||
|
||||
public let systemSize: Double
|
||||
@@ -42,8 +63,32 @@ public enum HeatingBalancePoint {
|
||||
}
|
||||
|
||||
public enum Response: Codable, Equatable, Sendable {
|
||||
case economic(Economic)
|
||||
case thermal(Thermal)
|
||||
|
||||
public struct Economic: Codable, Equatable, Sendable {
|
||||
|
||||
public let balancePointTemperature: Double
|
||||
public let fuelCostPerMMBTU: Double
|
||||
public let electricCostPerMMBTU: Double
|
||||
public let copAtBalancePoint: Double
|
||||
public let electricFuelRatio: Double
|
||||
|
||||
public init(
|
||||
balancePointTemperature: Double,
|
||||
fuelCostPerMMBTU: Double,
|
||||
electricCostPerMMBTU: Double,
|
||||
copAtBalancePoint: Double,
|
||||
electricFuelRatio: Double
|
||||
) {
|
||||
self.balancePointTemperature = balancePointTemperature
|
||||
self.fuelCostPerMMBTU = fuelCostPerMMBTU
|
||||
self.electricCostPerMMBTU = electricCostPerMMBTU
|
||||
self.copAtBalancePoint = copAtBalancePoint
|
||||
self.electricFuelRatio = electricFuelRatio
|
||||
}
|
||||
}
|
||||
|
||||
public struct Thermal: Codable, Equatable, Sendable {
|
||||
|
||||
public let capacityAt47: Double
|
||||
@@ -91,6 +136,26 @@ public enum HeatingBalancePoint {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum FuelType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case naturalGas
|
||||
case propane
|
||||
case oil
|
||||
|
||||
public var label: String {
|
||||
switch self {
|
||||
case .propane, .oil: return "\(rawValue.capitalized)"
|
||||
case .naturalGas: return "Natural Gas"
|
||||
}
|
||||
}
|
||||
|
||||
public var units: String {
|
||||
switch self {
|
||||
case .propane, .oil: return "gallons"
|
||||
case .naturalGas: return "therm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@@ -99,7 +164,13 @@ public enum HeatingBalancePoint {
|
||||
static func mock(mode: HeatingBalancePoint.Mode) -> Self {
|
||||
switch mode {
|
||||
case .economic:
|
||||
fatalError()
|
||||
return .economic(.init(
|
||||
balancePointTemperature: -10.8,
|
||||
fuelCostPerMMBTU: 27.6,
|
||||
electricCostPerMMBTU: 38.1,
|
||||
copAtBalancePoint: 2.24,
|
||||
electricFuelRatio: 1.38
|
||||
))
|
||||
case .thermal:
|
||||
return .thermal(.init(
|
||||
capacityAt47: 24600,
|
||||
|
||||
@@ -318,6 +318,17 @@ public extension SiteRoute {
|
||||
Method.post
|
||||
Body {
|
||||
OneOf {
|
||||
// Economic balance point
|
||||
FormData {
|
||||
Field("fuelType") { Routes.HeatingBalancePoint.FuelType.parser() }
|
||||
Field("fuelCostPerUnit") { Double.parser() }
|
||||
Field("fuelAFUE") { Double.parser() }
|
||||
Field("costPerKW") { Double.parser() }
|
||||
}
|
||||
.map(.memberwise(Routes.HeatingBalancePoint.Request.Economic.init))
|
||||
.map(.case(Routes.HeatingBalancePoint.Request.economic))
|
||||
|
||||
// Thermal Balance Point
|
||||
FormData {
|
||||
Field("systemSize") { Double.parser() }
|
||||
Optionally { Field("capacityAt47") { Double.parser() } }
|
||||
|
||||
@@ -41,11 +41,7 @@ struct HeatingBalancePointForm: HTML, Sendable {
|
||||
case .thermal:
|
||||
ThermalFields(heatLossMode: heatLossMode)
|
||||
case .economic:
|
||||
div {
|
||||
// FIX:
|
||||
WarningBox("This is still under development and may not be fully functional.")
|
||||
.attributes(.class("mb-6"))
|
||||
}
|
||||
EconomicFields()
|
||||
}
|
||||
|
||||
div {
|
||||
@@ -61,6 +57,29 @@ struct HeatingBalancePointForm: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
struct EconomicFields: HTML, Sendable {
|
||||
var content: some HTML {
|
||||
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) {
|
||||
div {
|
||||
InputLabel(for: "fuelType") { "Fuel Type" }
|
||||
Select(for: HeatingBalancePoint.FuelType.self, id: "fuelType") { $0.label }
|
||||
}
|
||||
LabeledContent(label: "AFUE (%)") {
|
||||
Input(id: "fuelAFUE", placeholder: "AFUE")
|
||||
.attributes(.type(.number), .min("1"), .max("100"), .step("0.5"), .value("90"), .required)
|
||||
}
|
||||
LabeledContent(label: "Fuel Cost (gallon or therm)") {
|
||||
Input(id: "fuelCostPerUnit", placeholder: "Fuel cost per unit")
|
||||
.attributes(.type(.number), .min("0.1"), .step("0.01"), .value("1.33"), .required)
|
||||
}
|
||||
LabeledContent(label: "Electric Cost (kw/h)") {
|
||||
Input(id: "costPerKW", placeholder: "Electric cost per kw/h")
|
||||
.attributes(.type(.number), .min("0.01"), .step("0.01"), .value("0.13"), .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ThermalFields: HTML, Sendable {
|
||||
let heatLossMode: HeatingBalancePoint.HeatLoss.Mode
|
||||
|
||||
@@ -175,6 +194,8 @@ struct HeatingBalancePointResponse: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
switch response {
|
||||
case let .economic(result):
|
||||
economicResult(result)
|
||||
case let .thermal(result):
|
||||
thermalResult(result)
|
||||
}
|
||||
@@ -184,6 +205,40 @@ struct HeatingBalancePointResponse: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
func economicResult(_ result: HeatingBalancePoint.Response.Economic) -> some HTML {
|
||||
div {
|
||||
VerticalGroup(
|
||||
label: "Balance Point",
|
||||
value: "\(double: result.balancePointTemperature, fractionDigits: 1)",
|
||||
valueLabel: "°F"
|
||||
)
|
||||
.attributes(.class("mb-8"))
|
||||
|
||||
div(.class("grid grid-cols-2 space-y-6")) {
|
||||
div {
|
||||
VerticalGroup(
|
||||
label: "Electric Cost",
|
||||
value: "$\(double: result.electricCostPerMMBTU, fractionDigits: 2)",
|
||||
valueLabel: "/ MMBTU"
|
||||
)
|
||||
}
|
||||
div {
|
||||
VerticalGroup(
|
||||
label: "Fuel Cost",
|
||||
value: "$\(double: result.fuelCostPerMMBTU, fractionDigits: 2)",
|
||||
valueLabel: "/ MMBTU"
|
||||
)
|
||||
}
|
||||
}
|
||||
div {
|
||||
VerticalGroup(
|
||||
label: "COP at Balance Point",
|
||||
value: "\(double: result.copAtBalancePoint, fractionDigits: 2)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func thermalResult(_ result: HeatingBalancePoint.Response.Thermal) -> some HTML {
|
||||
div {
|
||||
VerticalGroup(
|
||||
@@ -239,12 +294,14 @@ private extension HeatingBalancePoint.HeatLoss.Mode {
|
||||
private extension HeatingBalancePoint.Response {
|
||||
var mode: HeatingBalancePoint.Mode {
|
||||
switch self {
|
||||
case .economic: return .economic
|
||||
case .thermal: return .thermal
|
||||
}
|
||||
}
|
||||
|
||||
var warnings: [String] {
|
||||
switch self {
|
||||
case .economic: return []
|
||||
case let .thermal(result): return result.warnings
|
||||
}
|
||||
}
|
||||
|
||||
4
justfile
4
justfile
@@ -2,6 +2,10 @@ docker_image := "hvac-toolbox"
|
||||
docker_tag := "latest"
|
||||
docker_registiry := "registry.digitalocean.com/swift-hvac-toolbox"
|
||||
|
||||
[private]
|
||||
default:
|
||||
just --list
|
||||
|
||||
build-docker platform="linux/arm64":
|
||||
@docker build --platform {{platform}} -t {{docker_registiry}}/{{docker_image}}:{{docker_tag}} .
|
||||
|
||||
|
||||
Reference in New Issue
Block a user