feat: Begins thermal balance point
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,6 +1,11 @@
|
|||||||
public enum HeatingBalancePoint {
|
public enum HeatingBalancePoint {
|
||||||
|
|
||||||
|
public static let description: String = """
|
||||||
|
Calculate the heating balance point.
|
||||||
|
"""
|
||||||
|
|
||||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||||
|
case economic
|
||||||
case thermal
|
case thermal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,14 +18,14 @@ public enum HeatingBalancePoint {
|
|||||||
public let capacityAt47: Double?
|
public let capacityAt47: Double?
|
||||||
public let capacityAt17: Double?
|
public let capacityAt17: Double?
|
||||||
public let heatingDesignTemperature: Double
|
public let heatingDesignTemperature: Double
|
||||||
public let buildingHeatLoss: Double
|
public let buildingHeatLoss: HeatingBalancePoint.HeatLoss
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
systemSize: Double,
|
systemSize: Double,
|
||||||
capacityAt47: Double? = nil,
|
capacityAt47: Double? = nil,
|
||||||
capacityAt17: Double? = nil,
|
capacityAt17: Double? = nil,
|
||||||
heatingDesignTemperature: Double,
|
heatingDesignTemperature: Double,
|
||||||
buildingHeatLoss: Double
|
buildingHeatLoss: HeatingBalancePoint.HeatLoss
|
||||||
) {
|
) {
|
||||||
self.systemSize = systemSize
|
self.systemSize = systemSize
|
||||||
self.capacityAt47 = capacityAt47
|
self.capacityAt47 = capacityAt47
|
||||||
@@ -47,4 +52,15 @@ public enum HeatingBalancePoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum HeatLoss: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||||
|
case estimated
|
||||||
|
case known
|
||||||
|
}
|
||||||
|
|
||||||
|
case known(btu: Double)
|
||||||
|
case estimated(squareFeet: Double)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ public extension SiteRoute {
|
|||||||
case capacitor(Capacitor)
|
case capacitor(Capacitor)
|
||||||
case dehumidifierSize(DehumidifierSize)
|
case dehumidifierSize(DehumidifierSize)
|
||||||
case filterPressureDrop(FilterPressureDrop)
|
case filterPressureDrop(FilterPressureDrop)
|
||||||
|
case heatingBalancePoint(HeatingBalancePoint)
|
||||||
case hvacSystemPerformance(HVACSystemPerformance)
|
case hvacSystemPerformance(HVACSystemPerformance)
|
||||||
case moldRisk(MoldRisk)
|
case moldRisk(MoldRisk)
|
||||||
case roomPressure(RoomPressure)
|
case roomPressure(RoomPressure)
|
||||||
@@ -122,6 +123,9 @@ public extension SiteRoute {
|
|||||||
Route(.case(Self.filterPressureDrop)) {
|
Route(.case(Self.filterPressureDrop)) {
|
||||||
FilterPressureDrop.router
|
FilterPressureDrop.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.heatingBalancePoint)) {
|
||||||
|
HeatingBalancePoint.router
|
||||||
|
}
|
||||||
Route(.case(Self.hvacSystemPerformance)) {
|
Route(.case(Self.hvacSystemPerformance)) {
|
||||||
HVACSystemPerformance.router
|
HVACSystemPerformance.router
|
||||||
}
|
}
|
||||||
@@ -281,6 +285,56 @@ public extension SiteRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum HeatingBalancePoint: Equatable, Sendable {
|
||||||
|
case index(mode: Routes.HeatingBalancePoint.Mode? = nil, heatLossMode: Routes.HeatingBalancePoint.HeatLoss.Mode? = nil)
|
||||||
|
case heatLossFields(mode: Routes.HeatingBalancePoint.HeatLoss.Mode)
|
||||||
|
case submit(Routes.HeatingBalancePoint.Request)
|
||||||
|
|
||||||
|
public static var index: Self { index() }
|
||||||
|
|
||||||
|
static let rootPath = "balance-point"
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.index)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.get
|
||||||
|
Query {
|
||||||
|
Optionally { Field("mode") { Routes.HeatingBalancePoint.Mode.parser() } }
|
||||||
|
Optionally { Field("heatLossMode") { Routes.HeatingBalancePoint.HeatLoss.Mode.parser() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Route(.case(Self.heatLossFields)) {
|
||||||
|
Path { rootPath; "heat-loss" }
|
||||||
|
Method.get
|
||||||
|
Query {
|
||||||
|
Field("mode") { Routes.HeatingBalancePoint.HeatLoss.Mode.parser() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Route(.case(Self.submit)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.post
|
||||||
|
Body {
|
||||||
|
OneOf {
|
||||||
|
FormData {
|
||||||
|
Field("systemSize") { Double.parser() }
|
||||||
|
Optionally { Field("capcityAt47") { Double.parser() } }
|
||||||
|
Optionally { Field("capcityAt17") { Double.parser() } }
|
||||||
|
Field("heatingDesignTemperature") { Double.parser() }
|
||||||
|
OneOf {
|
||||||
|
Field("knownHeatLoss") { Double.parser() }
|
||||||
|
.map(.case(Routes.HeatingBalancePoint.HeatLoss.known))
|
||||||
|
Field("simplifiedHeatLoss") { Double.parser() }
|
||||||
|
.map(.case(Routes.HeatingBalancePoint.HeatLoss.estimated))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(.memberwise(Routes.HeatingBalancePoint.Request.Thermal.init))
|
||||||
|
.map(.case(Routes.HeatingBalancePoint.Request.thermal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum HVACSystemPerformance: Equatable, Sendable {
|
public enum HVACSystemPerformance: Equatable, Sendable {
|
||||||
case index
|
case index
|
||||||
case submit(Routes.HVACSystemPerformance.Request)
|
case submit(Routes.HVACSystemPerformance.Request)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public enum SVGType: Sendable, CaseIterable {
|
|||||||
case leftRightArrow
|
case leftRightArrow
|
||||||
case menu
|
case menu
|
||||||
case ruler
|
case ruler
|
||||||
|
case scale
|
||||||
case thermometer
|
case thermometer
|
||||||
case thermometerSun
|
case thermometerSun
|
||||||
case wind
|
case wind
|
||||||
@@ -69,6 +70,7 @@ public enum SVGType: Sendable, CaseIterable {
|
|||||||
case .leftRightArrow: return leftRightArrowSvg(size: size)
|
case .leftRightArrow: return leftRightArrowSvg(size: size)
|
||||||
case .menu: return menuSvg(size: size)
|
case .menu: return menuSvg(size: size)
|
||||||
case .ruler: return rulerSvg(size: size)
|
case .ruler: return rulerSvg(size: size)
|
||||||
|
case .scale: return scaleSvg(size: size)
|
||||||
case .thermometer: return thermometerSvg(size: size)
|
case .thermometer: return thermometerSvg(size: size)
|
||||||
case .thermometerSun: return thermometerSunSvg(size: size)
|
case .thermometerSun: return thermometerSunSvg(size: size)
|
||||||
case .wind: return windSvg(size: size)
|
case .wind: return windSvg(size: size)
|
||||||
@@ -82,6 +84,17 @@ public enum SVGType: Sendable, CaseIterable {
|
|||||||
|
|
||||||
// swiftlint:disable line_length
|
// swiftlint:disable line_length
|
||||||
|
|
||||||
|
private func scaleSvg(size: SVGSize) -> HTMLRaw {
|
||||||
|
HTMLRaw("""
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
|
||||||
|
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||||
|
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||||
|
<path d="M7 21h10"/><path d="M12 3v18"/>
|
||||||
|
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
|
||||||
|
</svg>
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
private func checkCircleSvg(size: SVGSize) -> HTMLRaw {
|
private func checkCircleSvg(size: SVGSize) -> HTMLRaw {
|
||||||
HTMLRaw("""
|
HTMLRaw("""
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-big">
|
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-big">
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ struct HomePage: HTML, Sendable {
|
|||||||
svg: .funnel,
|
svg: .funnel,
|
||||||
route: .filterPressureDrop(.index)
|
route: .filterPressureDrop(.index)
|
||||||
)
|
)
|
||||||
|
group(
|
||||||
|
label: "Heating Balance Point",
|
||||||
|
description: HeatingBalancePoint.description,
|
||||||
|
svg: .scale,
|
||||||
|
route: .heatingBalancePoint(.index)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,18 @@ extension ViewController: DependencyKey {
|
|||||||
return FilterPressureDropResponse(response: response)
|
return FilterPressureDropResponse(response: response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case let .heatingBalancePoint(route):
|
||||||
|
switch route {
|
||||||
|
case let .index(mode: mode, heatLossMode: heatLossMode):
|
||||||
|
return request.respond(HeatingBalancePointForm(mode: mode, heatLossMode: heatLossMode, response: nil))
|
||||||
|
case let .heatLossFields(mode: mode):
|
||||||
|
logger.debug("Heat loss mode: \(mode)")
|
||||||
|
return HeatingBalancePointForm.HeatLossFields(mode: mode)
|
||||||
|
case .submit:
|
||||||
|
// FIX:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
case let .hvacSystemPerformance(route):
|
case let .hvacSystemPerformance(route):
|
||||||
switch route {
|
switch route {
|
||||||
case .index:
|
case .index:
|
||||||
|
|||||||
143
Sources/ViewController/Views/HeatingBalancePoint.swift
Normal file
143
Sources/ViewController/Views/HeatingBalancePoint.swift
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import Routes
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
struct HeatingBalancePointForm: HTML, Sendable {
|
||||||
|
let mode: HeatingBalancePoint.Mode
|
||||||
|
let heatLossMode: HeatingBalancePoint.HeatLoss.Mode
|
||||||
|
let response: HeatingBalancePoint.Response?
|
||||||
|
|
||||||
|
init(
|
||||||
|
mode: HeatingBalancePoint.Mode?,
|
||||||
|
heatLossMode: HeatingBalancePoint.HeatLoss.Mode? = nil,
|
||||||
|
response: HeatingBalancePoint.Response? = nil
|
||||||
|
) {
|
||||||
|
self.mode = mode ?? .thermal
|
||||||
|
self.heatLossMode = heatLossMode ?? .estimated
|
||||||
|
self.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
FormHeader(label: "Balance Point - \(mode.label)", svg: .scale)
|
||||||
|
// TODO: Toggle button
|
||||||
|
|
||||||
|
form(
|
||||||
|
.hx.post(route: .heatingBalancePoint(.index)),
|
||||||
|
.hx.target("#result")
|
||||||
|
) {
|
||||||
|
div(.class("space-y-6")) {
|
||||||
|
switch mode {
|
||||||
|
case .thermal:
|
||||||
|
ThermalFields(heatLossMode: heatLossMode)
|
||||||
|
case .economic:
|
||||||
|
div {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
SubmitButton(label: "Calculate Balance Point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div(.id("result")) {
|
||||||
|
if let response {
|
||||||
|
HeatingBalancePointResponse(response: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThermalFields: HTML, Sendable {
|
||||||
|
let heatLossMode: HeatingBalancePoint.HeatLoss.Mode
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
LabeledContent(label: "System Size (Tons)") {
|
||||||
|
Input(id: "systemSize", placeholder: "System size")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"), .autofocus, .required)
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
div(.class("mb-4")) {
|
||||||
|
h4(.class("text-lg font-bold")) { "Capacities" }
|
||||||
|
p(.class("text-sm")) {
|
||||||
|
"Entering known capacities gives better results, otherwise capacities will be estimated."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) {
|
||||||
|
LabeledContent(label: "Capacity @ 47° (BTU/h)") {
|
||||||
|
Input(id: "capacityAt47", placeholder: "Capacity @ 47° (optional)")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"))
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Capacity @ 17° (BTU/h)") {
|
||||||
|
Input(id: "capacityAt17", placeholder: "Capacity @ 17° (optional)")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HeatLossFields(mode: heatLossMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HeatLossFields: HTML, Sendable {
|
||||||
|
let mode: HeatingBalancePoint.HeatLoss.Mode
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
div(.id("heatLossFields")) {
|
||||||
|
div(.class("flex flex-wrap justify-between")) {
|
||||||
|
h4(.class("text-lg font-bold")) { "Heat Loss - \(mode.label)" }
|
||||||
|
Toggle(
|
||||||
|
isOn: mode == .estimated,
|
||||||
|
onLabel: HeatingBalancePoint.HeatLoss.Mode.estimated.label,
|
||||||
|
onAttributes: [
|
||||||
|
.hx.get(route: .heatingBalancePoint(.heatLossFields(mode: .estimated))),
|
||||||
|
.hx.target("#heatLossFields")
|
||||||
|
],
|
||||||
|
offLabel: HeatingBalancePoint.HeatLoss.Mode.known.label,
|
||||||
|
offAttributes: [
|
||||||
|
.hx.get(route: .heatingBalancePoint(.heatLossFields(mode: .known))),
|
||||||
|
.hx.target("#heatLossFields")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.attributes(.class("mb-6"))
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case .known:
|
||||||
|
knownFields
|
||||||
|
case .estimated:
|
||||||
|
simplifiedFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var simplifiedFields: some HTML {
|
||||||
|
LabeledContent(label: "Building Size (ft²)") {
|
||||||
|
Input(id: "simplifiedHeatLoss", placeholder: "Square feet")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var knownFields: some HTML {
|
||||||
|
LabeledContent(label: "Heat Loss (BTU/h)") {
|
||||||
|
Input(id: "knownHeatLoss", placeholder: "Heat loss")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HeatingBalancePointResponse: HTML, Sendable {
|
||||||
|
|
||||||
|
let response: HeatingBalancePoint.Response
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension HeatingBalancePoint.Mode {
|
||||||
|
|
||||||
|
var label: String { rawValue.capitalized }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension HeatingBalancePoint.HeatLoss.Mode {
|
||||||
|
var label: String { rawValue.capitalized }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user