feat: Finishes economic balance point.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "af48ddc16f0ca460cff188345cf870056ca53edee23c016080fc1ad55f366bf9",
|
"originHash" : "16a33bebe1b38c2194b6ad57854e74c156212cd21d65cd12ccf6df7a6b89886c",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "async-http-client",
|
"identity" : "async-http-client",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
|||||||
import CoreModels
|
import CoreModels
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
|
import OpcostAnalysis
|
||||||
import Routes
|
import Routes
|
||||||
|
|
||||||
public extension HeatingBalancePoint.Request {
|
public extension HeatingBalancePoint.Request {
|
||||||
@@ -10,6 +11,42 @@ public extension HeatingBalancePoint.Request {
|
|||||||
case let .thermal(request):
|
case let .thermal(request):
|
||||||
logger.debug("Calculating thermal balance point: \(request)")
|
logger.debug("Calculating thermal balance point: \(request)")
|
||||||
return try await request.respond(logger: logger)
|
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
|
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(
|
private func thermalBalancePoint(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Vapor
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct BrowserSyncHandler: LifecycleHandler {
|
struct BrowserSyncHandler: LifecycleHandler {
|
||||||
func didBoot(_ application: Application) throws {
|
func didBoot(_ application: Application) throws {
|
||||||
|
if Environment.get("BROWSER_AUTO_RELOAD") != nil {
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(filePath: "/bin/sh")
|
process.executableURL = URL(filePath: "/bin/sh")
|
||||||
process.arguments = ["-c", "browser-sync reload"]
|
process.arguments = ["-c", "browser-sync reload"]
|
||||||
@@ -14,4 +15,5 @@ import Vapor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -12,8 +12,29 @@ public enum HeatingBalancePoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum Request: Codable, Equatable, Sendable {
|
public enum Request: Codable, Equatable, Sendable {
|
||||||
|
case economic(Economic)
|
||||||
case thermal(Thermal)
|
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 struct Thermal: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
public let systemSize: Double
|
public let systemSize: Double
|
||||||
@@ -42,8 +63,32 @@ public enum HeatingBalancePoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum Response: Codable, Equatable, Sendable {
|
public enum Response: Codable, Equatable, Sendable {
|
||||||
|
case economic(Economic)
|
||||||
case thermal(Thermal)
|
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 struct Thermal: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
public let capacityAt47: Double
|
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
|
#if DEBUG
|
||||||
@@ -99,7 +164,13 @@ public enum HeatingBalancePoint {
|
|||||||
static func mock(mode: HeatingBalancePoint.Mode) -> Self {
|
static func mock(mode: HeatingBalancePoint.Mode) -> Self {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .economic:
|
case .economic:
|
||||||
fatalError()
|
return .economic(.init(
|
||||||
|
balancePointTemperature: -10.8,
|
||||||
|
fuelCostPerMMBTU: 27.6,
|
||||||
|
electricCostPerMMBTU: 38.1,
|
||||||
|
copAtBalancePoint: 2.24,
|
||||||
|
electricFuelRatio: 1.38
|
||||||
|
))
|
||||||
case .thermal:
|
case .thermal:
|
||||||
return .thermal(.init(
|
return .thermal(.init(
|
||||||
capacityAt47: 24600,
|
capacityAt47: 24600,
|
||||||
|
|||||||
@@ -318,6 +318,17 @@ public extension SiteRoute {
|
|||||||
Method.post
|
Method.post
|
||||||
Body {
|
Body {
|
||||||
OneOf {
|
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 {
|
FormData {
|
||||||
Field("systemSize") { Double.parser() }
|
Field("systemSize") { Double.parser() }
|
||||||
Optionally { Field("capacityAt47") { Double.parser() } }
|
Optionally { Field("capacityAt47") { Double.parser() } }
|
||||||
|
|||||||
@@ -41,11 +41,7 @@ struct HeatingBalancePointForm: HTML, Sendable {
|
|||||||
case .thermal:
|
case .thermal:
|
||||||
ThermalFields(heatLossMode: heatLossMode)
|
ThermalFields(heatLossMode: heatLossMode)
|
||||||
case .economic:
|
case .economic:
|
||||||
div {
|
EconomicFields()
|
||||||
// FIX:
|
|
||||||
WarningBox("This is still under development and may not be fully functional.")
|
|
||||||
.attributes(.class("mb-6"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
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 {
|
struct ThermalFields: HTML, Sendable {
|
||||||
let heatLossMode: HeatingBalancePoint.HeatLoss.Mode
|
let heatLossMode: HeatingBalancePoint.HeatLoss.Mode
|
||||||
|
|
||||||
@@ -175,6 +194,8 @@ struct HeatingBalancePointResponse: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch response {
|
switch response {
|
||||||
|
case let .economic(result):
|
||||||
|
economicResult(result)
|
||||||
case let .thermal(result):
|
case let .thermal(result):
|
||||||
thermalResult(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 {
|
func thermalResult(_ result: HeatingBalancePoint.Response.Thermal) -> some HTML {
|
||||||
div {
|
div {
|
||||||
VerticalGroup(
|
VerticalGroup(
|
||||||
@@ -239,12 +294,14 @@ private extension HeatingBalancePoint.HeatLoss.Mode {
|
|||||||
private extension HeatingBalancePoint.Response {
|
private extension HeatingBalancePoint.Response {
|
||||||
var mode: HeatingBalancePoint.Mode {
|
var mode: HeatingBalancePoint.Mode {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .economic: return .economic
|
||||||
case .thermal: return .thermal
|
case .thermal: return .thermal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var warnings: [String] {
|
var warnings: [String] {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .economic: return []
|
||||||
case let .thermal(result): return result.warnings
|
case let .thermal(result): return result.warnings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
justfile
4
justfile
@@ -2,6 +2,10 @@ docker_image := "hvac-toolbox"
|
|||||||
docker_tag := "latest"
|
docker_tag := "latest"
|
||||||
docker_registiry := "registry.digitalocean.com/swift-hvac-toolbox"
|
docker_registiry := "registry.digitalocean.com/swift-hvac-toolbox"
|
||||||
|
|
||||||
|
[private]
|
||||||
|
default:
|
||||||
|
just --list
|
||||||
|
|
||||||
build-docker platform="linux/arm64":
|
build-docker platform="linux/arm64":
|
||||||
@docker build --platform {{platform}} -t {{docker_registiry}}/{{docker_image}}:{{docker_tag}} .
|
@docker build --platform {{platform}} -t {{docker_registiry}}/{{docker_image}}:{{docker_tag}} .
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user