feat: Working on pressure estimations feature, integrating all views and shared settings

This commit is contained in:
2024-06-10 13:34:38 -04:00
parent 51f7a30701
commit a6bfbd6877
15 changed files with 447 additions and 348 deletions

View File

@@ -66,15 +66,15 @@ public struct EstimatedPressureDependency {
extension EstimatedPressureDependency { extension EstimatedPressureDependency {
private func estimatedPressure( private func estimatedPressure(
existingPressure: Positive<Double?>, existingPressure: Positive<Double>,
existingAirflow: Double, existingAirflow: Positive<Double>,
targetAirflow: Double targetAirflow: Positive<Double>
) async throws -> Positive<Double> { ) async throws -> Positive<Double> {
try await self.estimatedPressure( try await self.estimatedPressure(
.init( .init(
existingPressure: existingPressure.positiveValue ?? 0, existingPressure: existingPressure.positiveValue,
existingAirflow: existingAirflow, existingAirflow: existingAirflow.positiveValue,
targetAirflow: targetAirflow targetAirflow: targetAirflow.positiveValue
) )
) )
} }
@@ -83,78 +83,75 @@ extension EstimatedPressureDependency {
equipmentMeasurement: EquipmentMeasurement, equipmentMeasurement: EquipmentMeasurement,
airflow updatedAirflow: Double airflow updatedAirflow: Double
) async throws -> EquipmentMeasurement { ) async throws -> EquipmentMeasurement {
let updatedAirflow = Positive(updatedAirflow)
switch equipmentMeasurement { switch equipmentMeasurement {
case let .airHandler(airHandler): case let .airHandler(airHandler):
guard let existingAirflow = airHandler.airflow else {
throw InvalidAirflow()
}
return try await .airHandler( return try await .airHandler(
.init( .init(
airflow: updatedAirflow, airflow: updatedAirflow,
manufacturersIncludedFilterPressureDrop: airHandler.$manufacturersIncludedFilterPressureDrop,
returnPlenumPressure: self.estimatedPressure( returnPlenumPressure: self.estimatedPressure(
existingPressure: airHandler.$returnPlenumPressure, existingPressure: airHandler.$returnPlenumPressure,
existingAirflow: existingAirflow, existingAirflow: airHandler.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
), ),
postFilterPressure: self.estimatedPressure( postFilterPressure: self.estimatedPressure(
existingPressure: airHandler.$postFilterPressure, existingPressure: airHandler.$postFilterPressure,
existingAirflow: existingAirflow, existingAirflow: airHandler.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
), ),
postCoilPressure: self.estimatedPressure( postCoilPressure: self.estimatedPressure(
existingPressure: airHandler.$postCoilPressure, existingPressure: airHandler.$postCoilPressure,
existingAirflow: existingAirflow, existingAirflow: airHandler.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
), ),
supplyPlenumPressure: self.estimatedPressure( supplyPlenumPressure: self.estimatedPressure(
existingPressure: airHandler.$supplyPlenumPressure, existingPressure: airHandler.$supplyPlenumPressure,
existingAirflow: existingAirflow, existingAirflow: airHandler.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
) )
) )
) )
case let .furnaceAndCoil(furnaceAndCoil): case let .furnaceAndCoil(furnaceAndCoil):
guard let existingAirflow = furnaceAndCoil.airflow else {
throw InvalidAirflow()
}
return try await .furnaceAndCoil( return try await .furnaceAndCoil(
.init( .init(
airflow: updatedAirflow, airflow: updatedAirflow,
manufacturersIncludedFilterPressureDrop: furnaceAndCoil.$manufacturersIncludedFilterPressureDrop,
returnPlenumPressure: self.estimatedPressure( returnPlenumPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$returnPlenumPressure, existingPressure: furnaceAndCoil.$returnPlenumPressure,
existingAirflow: existingAirflow, existingAirflow: furnaceAndCoil.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
), ),
postFilterPressure: self.estimatedPressure( postFilterPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$postFilterPressure, existingPressure: furnaceAndCoil.$postFilterPressure,
existingAirflow: existingAirflow, existingAirflow: furnaceAndCoil.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
), ),
preCoilPressure: self.estimatedPressure( preCoilPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$preCoilPressure, existingPressure: furnaceAndCoil.$preCoilPressure,
existingAirflow: existingAirflow, existingAirflow: furnaceAndCoil.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
), ),
supplyPlenumPressure: self.estimatedPressure( supplyPlenumPressure: self.estimatedPressure(
existingPressure: furnaceAndCoil.$supplyPlenumPressure, existingPressure: furnaceAndCoil.$supplyPlenumPressure,
existingAirflow: existingAirflow, existingAirflow: furnaceAndCoil.$airflow,
targetAirflow: updatedAirflow targetAirflow: updatedAirflow
) )
) )
) )
} }
} }
public func estimatedPressure( public func estimatedPressure(
equipmentMeasurement: EquipmentMeasurement, equipmentMeasurement: EquipmentMeasurement,
airflow updatedAirflow: Double, airflow updatedAirflow: Positive<Double>,
filterPressureDrop: Positive<Double> filterPressureDrop: Positive<Double>
) async throws -> EquipmentMeasurement { ) async throws -> EquipmentMeasurement {
let estimate = try await estimatedPressure( let estimate = try await estimatedPressure(
equipmentMeasurement: equipmentMeasurement, equipmentMeasurement: equipmentMeasurement,
airflow: updatedAirflow airflow: updatedAirflow.positiveValue
) )
switch estimate { switch estimate {
@@ -183,7 +180,7 @@ extension EstimatedPressureDependency {
return try await estimatedPressure( return try await estimatedPressure(
equipmentMeasurement: equipmentMeasurement, equipmentMeasurement: equipmentMeasurement,
airflow: updatedAirflow, airflow: updatedAirflow,
filterPressureDrop: filterPressureDrop filterPressureDrop: Positive(filterPressureDrop.positiveValue)
) )
} }
} }
@@ -219,42 +216,6 @@ extension EstimatedPressureDependency: DependencyKey {
} }
} }
fileprivate extension EquipmentMeasurement.AirHandler {
init(
airflow: Double? = nil,
returnPlenumPressure: Positive<Double>,
postFilterPressure: Positive<Double>,
postCoilPressure: Positive<Double>,
supplyPlenumPressure: Positive<Double>
) {
self.init(
airflow: airflow,
returnPlenumPressure: returnPlenumPressure.positiveValue,
postFilterPressure: postFilterPressure.positiveValue,
postCoilPressure: postCoilPressure.positiveValue,
supplyPlenumPressure: supplyPlenumPressure.positiveValue
)
}
}
fileprivate extension EquipmentMeasurement.FurnaceAndCoil {
init(
airflow: Double? = nil,
returnPlenumPressure: Positive<Double>,
postFilterPressure: Positive<Double>,
preCoilPressure: Positive<Double>,
supplyPlenumPressure: Positive<Double>
) {
self.init(
airflow: airflow,
returnPlenumPressure: returnPlenumPressure.positiveValue,
postFilterPressure: postFilterPressure.positiveValue,
preCoilPressure: preCoilPressure.positiveValue,
supplyPlenumPressure: supplyPlenumPressure.positiveValue
)
}
}
struct InvalidAirflow: Error { } struct InvalidAirflow: Error { }
enum LessThanZeroError: Error { enum LessThanZeroError: Error {

View File

@@ -145,6 +145,7 @@ fileprivate extension EquipmentMeasurement.FlaggedMeasurement {
#if DEBUG #if DEBUG
private let equipmentMeasurement = EquipmentMeasurement.airHandler(.init( private let equipmentMeasurement = EquipmentMeasurement.airHandler(.init(
airflow: 1600, airflow: 1600,
manufacturersIncludedFilterPressureDrop: 0.1,
returnPlenumPressure: 0.37, returnPlenumPressure: 0.37,
postFilterPressure: 0.78, postFilterPressure: 0.78,
postCoilPressure: 0.9, postCoilPressure: 0.9,

View File

@@ -1,9 +1,9 @@
import ComposableArchitecture import ComposableArchitecture
import DependenciesAdditions
import SharedModels import SharedModels
import Styleguide import Styleguide
import SwiftUI import SwiftUI
@Reducer @Reducer
public struct EquipmentMeasurementForm { public struct EquipmentMeasurementForm {
@@ -11,12 +11,14 @@ public struct EquipmentMeasurementForm {
@Reducer(state: .equatable) @Reducer(state: .equatable)
public enum Destination { public enum Destination {
case flaggedMeasurementsList(FlaggedMeasurementsList)
case infoView(InfoViewFeature) case infoView(InfoViewFeature)
} }
@ObservableState @ObservableState
public struct State: Equatable { public struct State: Equatable {
@Presents public var destination: Destination.State? @Presents public var destination: Destination.State?
@Shared public var sharedSettings: SharedPressureEstimationSettings
public var allowEquipmentTypeSelection: Bool public var allowEquipmentTypeSelection: Bool
public var equipmentType: EquipmentMeasurement.EquipmentType public var equipmentType: EquipmentMeasurement.EquipmentType
public var focusedField: Field? public var focusedField: Field?
@@ -25,12 +27,14 @@ public struct EquipmentMeasurementForm {
public init( public init(
allowEquipmentTypeSelection: Bool = true, allowEquipmentTypeSelection: Bool = true,
destination: Destination.State? = nil, destination: Destination.State? = nil,
sharedSettings: Shared<SharedPressureEstimationSettings>,
equipmentType: EquipmentMeasurement.EquipmentType = .airHandler, equipmentType: EquipmentMeasurement.EquipmentType = .airHandler,
focusedField: Field? = nil, focusedField: Field? = nil,
measurements: Measurements = .init() measurements: Measurements = .init()
) { ) {
self.allowEquipmentTypeSelection = allowEquipmentTypeSelection self.allowEquipmentTypeSelection = allowEquipmentTypeSelection
self.destination = destination self.destination = destination
self._sharedSettings = sharedSettings
self.equipmentType = equipmentType self.equipmentType = equipmentType
self.focusedField = focusedField self.focusedField = focusedField
self.measurements = measurements self.measurements = measurements
@@ -40,14 +44,27 @@ public struct EquipmentMeasurementForm {
measurements.equipmentMeasurement(type: equipmentType) measurements.equipmentMeasurement(type: equipmentType)
} }
public var isValid: Bool { private var baseValidations: Bool {
measurements.airflow != nil return measurements.airflow != nil
&& measurements.returnPlenumPressure != nil && measurements.returnPlenumPressure != nil
&& measurements.postFilterPressure != nil
&& measurements.coilPressure != nil
&& measurements.supplyPlenumPressure != nil && measurements.supplyPlenumPressure != nil
} }
private var furnaceAndCoilValidations: Bool {
return measurements.postFilterPressure != nil
&& measurements.coilPressure != nil
&& baseValidations
}
public var isValid: Bool {
switch equipmentType {
case .airHandler:
return baseValidations
case .furnaceAndCoil:
return furnaceAndCoilValidations
}
}
public struct Measurements: Equatable { public struct Measurements: Equatable {
public var airflow: Double? public var airflow: Double?
public var returnPlenumPressure: Double? public var returnPlenumPressure: Double?
@@ -96,23 +113,24 @@ public struct EquipmentMeasurementForm {
type: EquipmentMeasurement.EquipmentType type: EquipmentMeasurement.EquipmentType
) -> EquipmentMeasurement { ) -> EquipmentMeasurement {
switch type { switch type {
case .airHandler: case .airHandler:
return .airHandler(.init( return .airHandler(.init(
airflow: airflow, airflow: airflow ?? 0,
returnPlenumPressure: returnPlenumPressure, manufacturersIncludedFilterPressureDrop: 0.1,
postFilterPressure: postFilterPressure, returnPlenumPressure: returnPlenumPressure ?? 0,
postCoilPressure: coilPressure, postFilterPressure: postFilterPressure ?? 0,
supplyPlenumPressure: supplyPlenumPressure postCoilPressure: coilPressure ?? 0,
supplyPlenumPressure: supplyPlenumPressure ?? 0
)) ))
case .furnaceAndCoil: case .furnaceAndCoil:
return .furnaceAndCoil(.init( return .furnaceAndCoil(.init(
airflow: airflow, airflow: airflow ?? 0,
returnPlenumPressure: returnPlenumPressure, manufacturersIncludedFilterPressureDrop: 0,
postFilterPressure: postFilterPressure, returnPlenumPressure: returnPlenumPressure ?? 0,
preCoilPressure: coilPressure, postFilterPressure: postFilterPressure ?? 0,
supplyPlenumPressure: supplyPlenumPressure preCoilPressure: coilPressure ?? 0,
supplyPlenumPressure: supplyPlenumPressure ?? 0
)) ))
} }
} }
@@ -138,11 +156,15 @@ public struct EquipmentMeasurementForm {
@CasePathable @CasePathable
public enum View { public enum View {
case infoButtonTapped case infoButtonTapped
case nextButtonTapped
case onAppear
case resetButtonTapped case resetButtonTapped
case submitField case submitField
} }
} }
@Dependency(\.logger["\(Self.self)"]) var logger
public var body: some Reducer<State, Action> { public var body: some Reducer<State, Action> {
BindingReducer() BindingReducer()
Reduce<State, Action> { state, action in Reduce<State, Action> { state, action in
@@ -161,11 +183,36 @@ public struct EquipmentMeasurementForm {
state.destination = .infoView(.init()) state.destination = .infoView(.init())
return .none return .none
case .nextButtonTapped:
guard state.isValid else {
return .fail(
"""
Received next button tapped action, but the state is not valid.
This is considered an application logic error.
""",
logger: logger
)
}
state.sharedSettings.equipmentMeasurement = state.equipmentMeasurement
state.destination = .flaggedMeasurementsList(.init(sharedSettings: state.$sharedSettings))
return .none
case .onAppear:
guard let measurement = state.sharedSettings.equipmentMeasurement else {
return .none
}
state.measurements = .init(equipmentMeasurement: measurement)
return .none
case .resetButtonTapped: case .resetButtonTapped:
state.measurements = .init() state.measurements = .init()
return .none return .none
case .submitField: case .submitField:
if state.focusedField == .airflow {
return .send(.view(.nextButtonTapped))
}
state.focusedField = state.focusedField?.next state.focusedField = state.focusedField?.next
return .none return .none
@@ -183,6 +230,12 @@ extension Store where State == EquipmentMeasurementForm.State {
) -> String { ) -> String {
let label = label(field: field) let label = label(field: field)
guard field != .airflow else { return label } guard field != .airflow else { return label }
if field == .coilPressure && state.equipmentType == .airHandler {
return "\(label) (Optional)"
}
if field == .postFilterPressure && state.equipmentType == .airHandler {
return "\(label) (Optional)"
}
return "\(label) Pressure" return "\(label) Pressure"
} }
@@ -269,8 +322,14 @@ public struct EquipmentMeasurementFormView: View {
} }
} }
} }
.onAppear { send(.onAppear) }
.textLabelStyle(.boldSecondary) .textLabelStyle(.boldSecondary)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.toolbar {
NextButton { send(.nextButtonTapped) }
.nextButtonStyle(.toolbar)
.disabled(!store.isValid)
}
.sheet( .sheet(
item: $store.scope(state: \.destination?.infoView, action: \.destination.infoView) item: $store.scope(state: \.destination?.infoView, action: \.destination.infoView)
){ store in ){ store in
@@ -278,6 +337,14 @@ public struct EquipmentMeasurementFormView: View {
InfoView(store: store) InfoView(store: store)
} }
} }
.navigationDestination(
item: $store.scope(
state: \.destination?.flaggedMeasurementsList,
action: \.destination.flaggedMeasurementsList
)
) { store in
FlaggedMeasurementListView(store: store)
}
} }
private func textField( private func textField(
@@ -350,7 +417,9 @@ fileprivate extension InfoViewFeature.State {
#Preview { #Preview {
NavigationStack { NavigationStack {
EquipmentMeasurementFormView( EquipmentMeasurementFormView(
store: Store(initialState: EquipmentMeasurementForm.State()) { store: Store(initialState: EquipmentMeasurementForm.State(
sharedSettings: Shared(SharedPressureEstimationSettings()))
) {
EquipmentMeasurementForm() EquipmentMeasurementForm()
} }
) )

View File

@@ -4,13 +4,13 @@ import SharedModels
import Styleguide import Styleguide
import SwiftUI import SwiftUI
@Reducer @Reducer
public struct EquipmentSettingsForm { public struct EquipmentSettingsForm {
@CasePathable @CasePathable
public enum InfoView { public enum InfoView {
case capacities case capacities
case manufacturersIncludedFilterPressureDrop
case ratedStaticPressures case ratedStaticPressures
} }
@@ -21,34 +21,27 @@ public struct EquipmentSettingsForm {
@ObservableState @ObservableState
public struct State: Equatable { public struct State: Equatable {
@Presents public var destination: Destination.State? @Presents public var destination: Destination.State?
public var coolingCapacity: EquipmentMetadata.CoolingCapacity public var includesFilterDrop: Bool
public var equipmentType: EquipmentMeasurement.EquipmentType public var equipmentType: EquipmentMeasurement.EquipmentType
public var fanType: EquipmentMetadata.FanType
public var focusedField: Field? = nil public var focusedField: Field? = nil
public var heatingCapacity: Double? @Shared public var sharedSettings: SharedPressureEstimationSettings
public var ratedStaticPressures: RatedStaticPressures
public init( public init(
coolingCapacity: EquipmentMetadata.CoolingCapacity = .default,
destination: Destination.State? = nil, destination: Destination.State? = nil,
includesFilterDrop: Bool = false,
equipmentType: EquipmentMeasurement.EquipmentType = .airHandler, equipmentType: EquipmentMeasurement.EquipmentType = .airHandler,
fanType: EquipmentMetadata.FanType = .constantSpeed, sharedSettings: Shared<SharedPressureEstimationSettings>
heatingCapacity: Double? = nil,
ratedStaticPressures: RatedStaticPressures = .init()
) { ) {
self.coolingCapacity = coolingCapacity
self.destination = destination self.destination = destination
self.includesFilterDrop = includesFilterDrop
self.equipmentType = equipmentType self.equipmentType = equipmentType
self.fanType = fanType self._sharedSettings = sharedSettings
self.heatingCapacity = heatingCapacity
self.ratedStaticPressures = ratedStaticPressures
} }
public var isValid: Bool { public var isValid: Bool {
guard equipmentType == .furnaceAndCoil else { return true } guard equipmentType == .furnaceAndCoil else { return true }
return heatingCapacity != nil return sharedSettings.heatingCapacity != nil
} }
// Note: These need to be in display order. // Note: These need to be in display order.
@@ -57,6 +50,7 @@ public struct EquipmentSettingsForm {
case minimumStaticPressure case minimumStaticPressure
case maximumStaticPressure case maximumStaticPressure
case ratedStaticPressure case ratedStaticPressure
case manufacturersIncludedFilterPressureDrop
} }
} }
@@ -68,7 +62,6 @@ public struct EquipmentSettingsForm {
@CasePathable @CasePathable
public enum View { public enum View {
case infoButtonTapped(InfoView) case infoButtonTapped(InfoView)
case nextButtonTapped
case resetButtonTapped case resetButtonTapped
case submitField case submitField
} }
@@ -79,6 +72,17 @@ public struct EquipmentSettingsForm {
Reduce<State, Action> { state, action in Reduce<State, Action> { state, action in
switch action { switch action {
case .binding(\.includesFilterDrop):
guard state.includesFilterDrop else {
state.sharedSettings.manufacturersIncludedFilterPressureDrop = nil
return .none
}
guard state.sharedSettings.manufacturersIncludedFilterPressureDrop != nil else {
return .none
}
state.sharedSettings.manufacturersIncludedFilterPressureDrop = 0.1
return .none
case .binding: case .binding:
return .none return .none
@@ -96,12 +100,8 @@ public struct EquipmentSettingsForm {
state.destination = .infoView(.init(view: infoView)) state.destination = .infoView(.init(view: infoView))
return .none return .none
case .nextButtonTapped:
#warning("Fix me.")
return .none
case .resetButtonTapped: case .resetButtonTapped:
state.heatingCapacity = nil state.sharedSettings.heatingCapacity = nil
return .none return .none
case .submitField: case .submitField:
@@ -138,7 +138,7 @@ public struct EquipmentSettingsFormView: View {
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
Section { Section {
FanTypePicker(selection: $store.fanType) FanTypePicker(selection: $store.sharedSettings.fanType)
.pickerStyle(.segmented) .pickerStyle(.segmented)
} header: { } header: {
Text("Fan Type") Text("Fan Type")
@@ -154,20 +154,16 @@ public struct EquipmentSettingsFormView: View {
: "Capacity" : "Capacity"
) )
Spacer() Spacer()
CoolingCapacityPicker(selection: $store.coolingCapacity) CoolingCapacityPicker(
// Picker("Cooling Capcity", selection: $store.coolingCapacity) { selection: $store.sharedSettings.coolingCapacity
// ForEach(CoolingCapacity.allCases) { )
// Text($0.description)
// .tag($0)
// }
// }
} }
if store.equipmentType == .furnaceAndCoil { if store.equipmentType == .furnaceAndCoil {
GridRow { GridRow {
TextLabel("Heating") TextLabel("Heating")
textField( textField(
"Heating Capacity", "Heating Capacity",
value: $store.heatingCapacity, value: $store.sharedSettings.heatingCapacity,
fractionLength: 0 fractionLength: 0
) )
.focused($focusedField, equals: .heatingCapacity) .focused($focusedField, equals: .heatingCapacity)
@@ -190,7 +186,7 @@ public struct EquipmentSettingsFormView: View {
TextLabel("Minimum") TextLabel("Minimum")
textField( textField(
"Minimum Pressure", "Minimum Pressure",
value: $store.ratedStaticPressures.minimum, value: $store.sharedSettings.ratedStaticPressures.minimum,
fractionLength: 2 fractionLength: 2
) )
.focused($focusedField, equals: .minimumStaticPressure) .focused($focusedField, equals: .minimumStaticPressure)
@@ -200,7 +196,7 @@ public struct EquipmentSettingsFormView: View {
TextLabel("Maximum") TextLabel("Maximum")
textField( textField(
"Maximum Pressure", "Maximum Pressure",
value: $store.ratedStaticPressures.maximum, value: $store.sharedSettings.ratedStaticPressures.maximum,
fractionLength: 2 fractionLength: 2
) )
.focused($focusedField, equals: .maximumStaticPressure) .focused($focusedField, equals: .maximumStaticPressure)
@@ -210,7 +206,7 @@ public struct EquipmentSettingsFormView: View {
TextLabel("Rated") TextLabel("Rated")
textField( textField(
"Rated Pressure", "Rated Pressure",
value: $store.ratedStaticPressures.rated, value: $store.sharedSettings.ratedStaticPressures.rated,
fractionLength: 2 fractionLength: 2
) )
.focused($focusedField, equals: .ratedStaticPressure) .focused($focusedField, equals: .ratedStaticPressure)
@@ -221,74 +217,36 @@ public struct EquipmentSettingsFormView: View {
header("Rated Static Pressure", infoView: .ratedStaticPressures) header("Rated Static Pressure", infoView: .ratedStaticPressures)
} }
// Section { Section {
// Grid(alignment: .leading, horizontalSpacing: 40) { VStack(alignment: .leading) {
// if store.equipmentMeasurement.equipmentType == .furnaceAndCoil { HStack {
// GridRow { TextLabel("Includes Filter Drop")
// TextLabel("Coil") Spacer()
// percentField("Coil Budget", value: $store.budgets.coilBudget.fraction) Toggle("Includes Filter Drop", isOn: $store.includesFilterDrop)
// .focused($focusedField, equals: .coilBudget) }
// .onSubmit { send(.submitField) } if store.includesFilterDrop {
// .numberPad() HStack {
// } TextLabel("Filter Drop")
// } Spacer()
// textField(
// GridRow { "Filter Drop",
// TextLabel("Filter") value: $store.sharedSettings.manufacturersIncludedFilterPressureDrop,
// percentField("Filter Budget", value: $store.budgets.filterBudget.fraction) fractionLength: 2
// .focused($focusedField, equals: .filterBudget) )
// .onSubmit { send(.submitField) } .focused($focusedField, equals: .manufacturersIncludedFilterPressureDrop)
// .numberPad() .decimalPad()
// } .padding(.leading, 40)
// }
// GridRow { }
// TextLabel("Return") }
// percentField("Return Plenum Budget", value: $store.budgets.returnPlenumBudget.fraction) } header: {
// .focused($focusedField, equals: .returnPlenumBudget) header(infoView: .manufacturersIncludedFilterPressureDrop) {
// .onSubmit { send(.submitField) } VStack(alignment: .leading) {
// .numberPad() Text("Manufacturer's")
// } Text("Filter Pressure Drop")
// }
// GridRow { }
// TextLabel("Supply") }
// percentField("Supply Plenum Budget", value: $store.budgets.supplyPlenumBudget.fraction)
// .focused($focusedField, equals: .supplyPlenumBudget)
// .onSubmit { send(.submitField) }
// .numberPad()
// }
// }
//
// } header: {
// VStack {
// header("Budgets", infoView: .budgets)
// #if os(macOS)
// .font(.title2)
// .padding(.top, 20)
// #endif
// FlaggedView(
// flagged: Flagged(wrappedValue: store.budgets.total.rawValue, .percent())
// )
// .flaggedViewStyle(BudgetFlagViewStyle())
// }
// }
// Section {
// TextField(
// "Airflow",
// value: $store.updatedAirflow,
// fractionLength: 0
// )
// } header: {
// header("Updated Airflow", infoView: .updatedAirflow)
// } footer: {
// HStack {
// ResetButton { send(.resetButtonTapped) }
// Spacer()
// NextButton { send(.nextButtonTapped) }
// .disabled(!store.isValid)
// }
// .padding(.top)
// }
} }
.labelsHidden() .labelsHidden()
.bind($focusedField, to: $store.focusedField) .bind($focusedField, to: $store.focusedField)
@@ -306,23 +264,23 @@ public struct EquipmentSettingsFormView: View {
} }
} }
private func header( private func header<Label: View>(
_ title: String, infoView: EquipmentSettingsForm.InfoView,
infoView: EquipmentSettingsForm.InfoView label: @escaping () -> Label
) -> some View { ) -> some View {
HStack { HStack {
Text(title) label()
Spacer() Spacer()
InfoButton { send(.infoButtonTapped(infoView)) } InfoButton { send(.infoButtonTapped(infoView)) }
} }
} }
// private func percentField( private func header(
// _ title: LocalizedStringKey, _ title: String,
// value: Binding<Double> infoView: EquipmentSettingsForm.InfoView
// ) -> some View { ) -> some View {
// TextField(title, value: value, format: .percent, prompt: Text(title)) header(infoView: infoView) { Text(title) }
// } }
private func textField( private func textField(
_ title: String, _ title: String,
@@ -362,37 +320,29 @@ fileprivate extension InfoViewFeature.State {
Record the cooling and heating capacities of the system. Record the cooling and heating capacities of the system.
""" """
) )
// case .budgets: case .manufacturersIncludedFilterPressureDrop:
// self.init( self.init(
// title: "Budgets", title: "Filter Pressure Drop",
// body: """ body: """
// Budgeted percentages for static pressure estimations, these generally are set to Record the filter pressure drop that the manufacturer includes in their blower performance table, if applicable.
// reasonable defaults, however you can change them if desired.
// Sometimes this information is not listed, therefore it may be reasonable to use a sensible default value of '0.1'.
// Note: These must total up to 100%.
// """ Note: The value that is set get's deducted from the filter pressure drop when determining the external static pressure of a system.
// ) """
)
case .ratedStaticPressures: case .ratedStaticPressures:
self.init( self.init(
title: "Rated Static", title: "Rated Static",
body: """ body: """
Record the rated static pressures of the system. Record the rated static pressures of the system.
These are generally found on the nameplate of the equipment or in the installation These are generally found on the nameplate of the equipment or in the installation manual.
manual.
The defaults are generally acceptable for most unitary equipment. The defaults are generally acceptable for most unitary equipment.
""" """
) )
// case .updatedAirflow:
// self.init(
// title: "Updated Airflow",
// body: """
// This is used to generated estimated static pressures at the updated airflow
// compared to the existing airflow of the system.
// """
// )
} }
} }
} }
@@ -401,9 +351,11 @@ fileprivate extension InfoViewFeature.State {
NavigationStack { NavigationStack {
EquipmentSettingsFormView( EquipmentSettingsFormView(
store: Store( store: Store(
initialState: EquipmentSettingsForm.State() initialState: EquipmentSettingsForm.State(
sharedSettings: Shared(SharedPressureEstimationSettings())
)
) { ) {
EquipmentSettingsForm() EquipmentSettingsForm()._printChanges()
} }
) )
#if os(macOS) #if os(macOS)

View File

@@ -20,12 +20,12 @@ public struct FlaggedMeasurementsList {
public struct State: Equatable { public struct State: Equatable {
@Presents public var destination: Destination.State? @Presents public var destination: Destination.State?
@Shared var sharedSettings: SharedSettings @Shared var sharedSettings: SharedPressureEstimationSettings
public var estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer> public var estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer>
init( init(
destination: Destination.State? = nil, destination: Destination.State? = nil,
sharedSettings: Shared<SharedSettings>, sharedSettings: Shared<SharedPressureEstimationSettings>,
estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer> = [] estimatedMeasurements: IdentifiedArrayOf<FlaggedMeasurementContainer> = []
) { ) {
self.destination = destination self.destination = destination
@@ -33,6 +33,7 @@ public struct FlaggedMeasurementsList {
self.estimatedMeasurements = estimatedMeasurements self.estimatedMeasurements = estimatedMeasurements
} }
#warning("Move to shared settings.")
public struct FlaggedMeasurementContainer: Equatable, Identifiable { public struct FlaggedMeasurementContainer: Equatable, Identifiable {
public let id: UUID public let id: UUID
public var flaggedMeasurement: EquipmentMeasurement.FlaggedMeasurement public var flaggedMeasurement: EquipmentMeasurement.FlaggedMeasurement
@@ -115,7 +116,7 @@ public struct FlaggedMeasurementsList {
case .addButtonTapped: case .addButtonTapped:
state.destination = .estimationForm(.init( state.destination = .estimationForm(.init(
coolingCapacity: state.sharedSettings.coolingCapacity coolingCapacity: state.sharedSettings.equipmentMetadata.coolingCapacity
)) ))
return .none return .none
@@ -145,16 +146,20 @@ public struct FlaggedMeasurementsList {
return .none return .none
case .onAppear: case .onAppear:
guard let equipmentMeasurement = state.sharedSettings.equipmentMeasurement, guard let equipmentMeasurement = state.sharedSettings.equipmentMeasurement else {
let budgets = state.sharedSettings.budgets
else {
return .none return .none
} }
if state.sharedSettings.budgets == nil {
state.sharedSettings.budgets = .init(
equipmentType: state.sharedSettings.equipmentMeasurement!.equipmentType,
fanType: state.sharedSettings.equipmentMetadata.fanType
)
}
state.sharedSettings.flaggedEquipmentMeasurement = .init( state.sharedSettings.flaggedEquipmentMeasurement = .init(
budgets: budgets, budgets: state.sharedSettings.budgets!,
measurement: equipmentMeasurement, measurement: equipmentMeasurement,
ratedPressures: state.sharedSettings.ratedStaticPressures, ratedPressures: state.sharedSettings.equipmentMetadata.ratedStaticPressures,
tons: state.sharedSettings.coolingCapacity tons: state.sharedSettings.equipmentMetadata.coolingCapacity
) )
return .none return .none
} }
@@ -178,7 +183,7 @@ public struct FlaggedMeasurementsList {
) )
} }
return .receive(action: \.receive) { [ratedStaticPressures = state.sharedSettings.ratedStaticPressures] in return .receive(action: \.receive) { [ratedStaticPressures = state.sharedSettings.equipmentMetadata.ratedStaticPressures] in
let filterPressureDrop = form.filterPressureDrop != nil let filterPressureDrop = form.filterPressureDrop != nil
? Positive(wrappedValue: form.filterPressureDrop!) ? Positive(wrappedValue: form.filterPressureDrop!)
@@ -221,8 +226,6 @@ public struct FlaggedMeasurementListView: View {
} header: { } header: {
HStack { HStack {
Text("Existing Measurements") Text("Existing Measurements")
Spacer()
// Button("Edit") { }
} }
} }
} }
@@ -278,7 +281,7 @@ public struct FlaggedMeasurementListView: View {
} }
#if DEBUG #if DEBUG
private let sharedSettings = SharedSettings( private let sharedPressureEstimationSettings = SharedPressureEstimationSettings(
budgets: .init(equipmentType: .airHandler, fanType: .constantSpeed), budgets: .init(equipmentType: .airHandler, fanType: .constantSpeed),
equipmentMeasurement: .mock(type: .airHandler), equipmentMeasurement: .mock(type: .airHandler),
flaggedEquipmentMeasurement: nil flaggedEquipmentMeasurement: nil
@@ -290,10 +293,10 @@ private let flaggedMeasurements = IdentifiedArrayOf<FlaggedMeasurementsList.Stat
id: UUID(0), id: UUID(0),
name: "Existing", name: "Existing",
flaggedMeasurement: .init( flaggedMeasurement: .init(
budgets: sharedSettings.budgets!, budgets: sharedPressureEstimationSettings.budgets!,
measurement: sharedSettings.equipmentMeasurement!, measurement: sharedPressureEstimationSettings.equipmentMeasurement!,
ratedPressures: sharedSettings.ratedStaticPressures, ratedPressures: sharedPressureEstimationSettings.equipmentMetadata.ratedStaticPressures,
tons: sharedSettings.coolingCapacity tons: sharedPressureEstimationSettings.equipmentMetadata.coolingCapacity
) )
), ),
] ]
@@ -305,7 +308,7 @@ private let flaggedMeasurements = IdentifiedArrayOf<FlaggedMeasurementsList.Stat
FlaggedMeasurementListView( FlaggedMeasurementListView(
store: Store( store: Store(
initialState: FlaggedMeasurementsList.State( initialState: FlaggedMeasurementsList.State(
sharedSettings: Shared(sharedSettings) sharedSettings: Shared(sharedPressureEstimationSettings)
) )
) { ) {
FlaggedMeasurementsList() FlaggedMeasurementsList()
@@ -319,7 +322,7 @@ private let flaggedMeasurements = IdentifiedArrayOf<FlaggedMeasurementsList.Stat
FlaggedMeasurementListView( FlaggedMeasurementListView(
store: Store( store: Store(
initialState: FlaggedMeasurementsList.State( initialState: FlaggedMeasurementsList.State(
sharedSettings: Shared(sharedSettings) sharedSettings: Shared(sharedPressureEstimationSettings)
) )
) { ) {
FlaggedMeasurementsList() FlaggedMeasurementsList()

View File

@@ -1,9 +1,11 @@
import ComposableArchitecture import ComposableArchitecture
import DependenciesAdditions
import SharedModels import SharedModels
import Styleguide import Styleguide
import SwiftUI import SwiftUI
import TCAExtras import TCAExtras
#warning("Fix shared settings.")
@Reducer @Reducer
public struct PressureEstimationsFeature { public struct PressureEstimationsFeature {
@@ -17,17 +19,16 @@ public struct PressureEstimationsFeature {
@ObservableState @ObservableState
public struct State: Equatable { public struct State: Equatable {
@Presents public var destination: Destination.State? @Presents public var destination: Destination.State?
@Shared(.sharedPressureEstimationSettings) var sharedSettings = SharedPressureEstimationSettings()
public var equipmentSettings: EquipmentSettingsForm.State public var equipmentSettings: EquipmentSettingsForm.State
public var equipmentMeasurements: EquipmentMeasurementForm.State?
public init( public init(
destination: Destination.State? = nil, destination: Destination.State? = nil,
equipmentSettings: EquipmentSettingsForm.State = .init(), sharedSettings: SharedPressureEstimationSettings = .init()
equipmentMeasurements: EquipmentMeasurementForm.State? = nil
) { ) {
self.destination = destination self.destination = destination
self.equipmentSettings = equipmentSettings self._sharedSettings = Shared(sharedSettings)
self.equipmentMeasurements = equipmentMeasurements self._equipmentSettings = .init(sharedSettings: self._sharedSettings)
} }
} }
@@ -42,6 +43,8 @@ public struct PressureEstimationsFeature {
} }
} }
@Dependency(\.logger["\(Self.self)"]) var logger
public var body: some Reducer<State, Action> { public var body: some Reducer<State, Action> {
Scope(state: \.equipmentSettings, action: \.equipmentSettings) { Scope(state: \.equipmentSettings, action: \.equipmentSettings) {
EquipmentSettingsForm() EquipmentSettingsForm()
@@ -57,13 +60,42 @@ public struct PressureEstimationsFeature {
case let .view(action): case let .view(action):
switch action { switch action {
case .nextButtonTapped: case .nextButtonTapped:
return .none return handleNextButtonTapped(&state)
} }
} }
} }
.ifLet(\.$destination, action: \.destination) .ifLet(\.$destination, action: \.destination)
} }
private func handleNextButtonTapped(_ state: inout State) -> Effect<Action> {
guard state.destination == nil else {
return .fail(
"""
Received next button tapped action on equipment settings form, but the destination is not nil.
This is considered an application logic error.
""",
logger: logger
)
}
guard state.equipmentSettings.isValid else {
return .fail(
"""
Received next button tapped action on equipment settings form, but the form is invalid.
This is considered an application logic error.
""",
logger: logger
)
}
state.destination = .equipmentMeasurements(.init(
allowEquipmentTypeSelection: false,
sharedSettings: state.$sharedSettings,
equipmentType: state.equipmentSettings.equipmentType
))
return .none
}
} }
@ViewAction(for: PressureEstimationsFeature.self) @ViewAction(for: PressureEstimationsFeature.self)
@@ -79,5 +111,30 @@ public struct PressureEstimationsView: View {
EquipmentSettingsFormView( EquipmentSettingsFormView(
store: store.scope(state: \.equipmentSettings, action: \.equipmentSettings) store: store.scope(state: \.equipmentSettings, action: \.equipmentSettings)
) )
.navigationTitle("Equipment Settings")
.toolbar {
NextButton { send(.nextButtonTapped) }
.nextButtonStyle(.toolbar)
.disabled(!store.equipmentSettings.isValid)
}
.navigationDestination(
item: $store.scope(
state: \.destination?.equipmentMeasurements,
action: \.destination.equipmentMeasurements
)
) { measurementStore in
EquipmentMeasurementFormView(store: measurementStore)
.navigationTitle("Existing Measurements")
}
}
}
#Preview {
NavigationStack {
PressureEstimationsView(
store: Store(initialState: PressureEstimationsFeature.State()) {
PressureEstimationsFeature()._printChanges()
}
)
} }
} }

View File

@@ -0,0 +1,40 @@
import ComposableArchitecture
import SharedModels
/// Holds onto shared values for several of the views in this feature.
@dynamicMemberLookup
public struct SharedPressureEstimationSettings: Equatable {
public var budgets: BudgetedPercentEnvelope?
public var equipmentMeasurement: EquipmentMeasurement?
public var equipmentMetadata: EquipmentMetadata
public var flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement?
public var heatingCapacity: Double?
public var manufacturersIncludedFilterPressureDrop: Double?
public init(
budgets: BudgetedPercentEnvelope? = nil,
equipmentMeasurement: EquipmentMeasurement? = nil,
equipmentMetadata: EquipmentMetadata = .init(),
flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? = nil,
heatingCapacity: Double? = nil,
manufacturersIncludedFilterPressureDrop: Double? = nil
) {
self.budgets = budgets
self.equipmentMeasurement = equipmentMeasurement
self.equipmentMetadata = equipmentMetadata
self.flaggedEquipmentMeasurement = flaggedEquipmentMeasurement
self.heatingCapacity = heatingCapacity
self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop
}
public subscript<T>(dynamicMember keyPath: WritableKeyPath<EquipmentMetadata, T>) -> T {
get { equipmentMetadata[keyPath: keyPath] }
set { equipmentMetadata[keyPath: keyPath] = newValue }
}
}
extension PersistenceReaderKey where Self == InMemoryKey<SharedPressureEstimationSettings> {
static var sharedPressureEstimationSettings: Self {
.inMemory("sharedPressureEstimationSettings")
}
}

View File

@@ -1,37 +0,0 @@
import ComposableArchitecture
import SharedModels
struct SharedSettings: Equatable {
var budgets: BudgetedPercentEnvelope?
var coolingCapacity: EquipmentMetadata.CoolingCapacity
var equipmentMeasurement: EquipmentMeasurement?
var fanType: EquipmentMetadata.FanType
var flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement?
var heatingCapacity: Double?
var ratedStaticPressures: RatedStaticPressures
init(
budgets: BudgetedPercentEnvelope? = nil,
coolingCapacity: EquipmentMetadata.CoolingCapacity = .default,
equipmentMeasurement: EquipmentMeasurement? = nil,
fanType: EquipmentMetadata.FanType = .constantSpeed,
flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? = nil,
heatingCapacity: Double? = nil,
ratedStaticPressures: RatedStaticPressures = .init()
) {
self.budgets = budgets
self.coolingCapacity = coolingCapacity
self.equipmentMeasurement = equipmentMeasurement
self.fanType = fanType
self.flaggedEquipmentMeasurement = flaggedEquipmentMeasurement
self.heatingCapacity = heatingCapacity
self.ratedStaticPressures = ratedStaticPressures
}
}
extension PersistenceReaderKey where Self == InMemoryKey<SharedSettings> {
static var sharedSettings: Self {
.inMemory("sharedSettings")
}
}

View File

@@ -1,2 +0,0 @@
import Foundation

View File

@@ -1,8 +1,5 @@
import Foundation import Foundation
#warning("Make values non-optional")
#warning("Need to make air handler external static handle large filter pressure drops.")
#warning("Add an exterenal static pressure strategy for if the filter is built-in or external")
public enum EquipmentMeasurement: Equatable { public enum EquipmentMeasurement: Equatable {
case airHandler(AirHandler) case airHandler(AirHandler)
@@ -17,7 +14,7 @@ public enum EquipmentMeasurement: Equatable {
} }
} }
public var externalStaticPressure: Double { public var externalStaticPressure: Positive<Double> {
switch self { switch self {
case let .airHandler(airHandler): case let .airHandler(airHandler):
return airHandler.externalStaticPressure return airHandler.externalStaticPressure
@@ -26,47 +23,75 @@ public enum EquipmentMeasurement: Equatable {
} }
} }
public var manufacturersIncludedFilterPressureDrop: Positive<Double> {
switch self {
case let .airHandler(airHandler):
return airHandler.$manufacturersIncludedFilterPressureDrop
case let .furnaceAndCoil(furnaceAndCoil):
return furnaceAndCoil.$manufacturersIncludedFilterPressureDrop
}
}
public struct AirHandler: Equatable { public struct AirHandler: Equatable {
@Positive @Positive
public var airflow: Double? public var airflow: Double
@Positive @Positive
public var returnPlenumPressure: Double? public var manufacturersIncludedFilterPressureDrop: Double
@Positive @Positive
public var postFilterPressure: Double? public var returnPlenumPressure: Double
@Positive @Positive
public var postCoilPressure: Double? public var postFilterPressure: Double
@Positive @Positive
public var supplyPlenumPressure: Double? public var postCoilPressure: Double
@Positive
public var supplyPlenumPressure: Double
public init( public init(
airflow: Double? = nil, airflow: Double,
returnPlenumPressure: Double? = nil, manufacturersIncludedFilterPressureDrop: Double,
postFilterPressure: Double? = nil, returnPlenumPressure: Double,
postCoilPressure: Double? = nil, postFilterPressure: Double,
supplyPlenumPressure: Double? = nil postCoilPressure: Double,
supplyPlenumPressure: Double
) { ) {
self.airflow = airflow self.airflow = airflow
self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop
self.returnPlenumPressure = returnPlenumPressure self.returnPlenumPressure = returnPlenumPressure
self.postFilterPressure = postFilterPressure self.postFilterPressure = postFilterPressure
self.postCoilPressure = postCoilPressure self.postCoilPressure = postCoilPressure
self.supplyPlenumPressure = supplyPlenumPressure self.supplyPlenumPressure = supplyPlenumPressure
} }
public var externalStaticPressure: Double { public init(
var postFilterAdder = 0.0 airflow: Positive<Double>,
if let postFilterPressure = $postFilterPressure.positiveValue, manufacturersIncludedFilterPressureDrop: Positive<Double>,
postFilterPressure > 0.1 returnPlenumPressure: Positive<Double>,
{ postFilterPressure: Positive<Double>,
postFilterAdder = postFilterPressure - 0.1 postCoilPressure: Positive<Double>,
supplyPlenumPressure: Positive<Double>
) {
self._airflow = airflow
self._manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop
self._returnPlenumPressure = returnPlenumPressure
self._postFilterPressure = postFilterPressure
self._postCoilPressure = postCoilPressure
self._supplyPlenumPressure = supplyPlenumPressure
}
public var externalStaticPressure: Positive<Double> {
var postFilterAdder = Positive<Double>.zero
if $postFilterPressure > $manufacturersIncludedFilterPressureDrop {
postFilterAdder = $postFilterPressure - $manufacturersIncludedFilterPressureDrop
} }
return ($returnPlenumPressure.positiveValue ?? 0) return $returnPlenumPressure
+ postFilterAdder + postFilterAdder
+ ($supplyPlenumPressure.positiveValue ?? 0) + $supplyPlenumPressure
} }
} }
@@ -145,7 +170,7 @@ public enum EquipmentMeasurement: Equatable {
.init( .init(
airflow: checkAirflow(value: measurement.airflow, tons: tons), airflow: checkAirflow(value: measurement.airflow, tons: tons),
coilPressureDrop: .init( coilPressureDrop: .init(
wrappedValue: (measurement.$postCoilPressure.positiveValue - measurement.$postFilterPressure.positiveValue) ?? 0, wrappedValue: (measurement.$postCoilPressure.positiveValue - measurement.$postFilterPressure.positiveValue),
.result(.good()) .result(.good())
), ),
externalStaticPressure: checkExternalStaticPressure( externalStaticPressure: checkExternalStaticPressure(
@@ -155,7 +180,8 @@ public enum EquipmentMeasurement: Equatable {
filterPressureDrop: .init( filterPressureDrop: .init(
value: measurement.$postFilterPressure.positiveValue - measurement.$returnPlenumPressure.positiveValue, value: measurement.$postFilterPressure.positiveValue - measurement.$returnPlenumPressure.positiveValue,
budget: budgets.filterBudget, budget: budgets.filterBudget,
ratedPressures: ratedPressures ratedPressures: ratedPressures,
ignoreMinimum: true
), ),
returnPlenumPressure: .init( returnPlenumPressure: .init(
value: measurement.$returnPlenumPressure.positiveValue, value: measurement.$returnPlenumPressure.positiveValue,
@@ -163,7 +189,7 @@ public enum EquipmentMeasurement: Equatable {
ratedPressures: ratedPressures ratedPressures: ratedPressures
), ),
supplyPlenumPressure: .init( supplyPlenumPressure: .init(
value: measurement.$supplyPlenumPressure.positiveValue ?? 0, value: measurement.$supplyPlenumPressure.positiveValue,
budget: budgets.supplyPlenumBudget, budget: budgets.supplyPlenumBudget,
ratedPressures: ratedPressures ratedPressures: ratedPressures
) )
@@ -211,61 +237,61 @@ public enum EquipmentMeasurement: Equatable {
public struct FurnaceAndCoil: Equatable { public struct FurnaceAndCoil: Equatable {
@Positive @Positive
public var airflow: Double? public var airflow: Double
@Positive @Positive
public var returnPlenumPressure: Double? public var manufacturersIncludedFilterPressureDrop: Double
@Positive @Positive
public var postFilterPressure: Double? public var returnPlenumPressure: Double
@Positive @Positive
public var preCoilPressure: Double? public var postFilterPressure: Double
@Positive @Positive
public var supplyPlenumPressure: Double? public var preCoilPressure: Double
@Positive
public var supplyPlenumPressure: Double
public init( public init(
airflow: Double? = nil, airflow: Double,
returnPlenumPressure: Double? = nil, manufacturersIncludedFilterPressureDrop: Double,
postFilterPressure: Double? = nil, returnPlenumPressure: Double,
preCoilPressure: Double? = nil, postFilterPressure: Double,
supplyPlenumPressure: Double? = nil preCoilPressure: Double,
supplyPlenumPressure: Double
) { ) {
self.airflow = airflow self.airflow = airflow
self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop
self.returnPlenumPressure = returnPlenumPressure self.returnPlenumPressure = returnPlenumPressure
self.postFilterPressure = postFilterPressure self.postFilterPressure = postFilterPressure
self.preCoilPressure = preCoilPressure self.preCoilPressure = preCoilPressure
self.supplyPlenumPressure = supplyPlenumPressure self.supplyPlenumPressure = supplyPlenumPressure
} }
public var externalStaticPressure: Double { public init(
($postFilterPressure.positiveValue ?? 0) + ($preCoilPressure.positiveValue ?? 0) airflow: Positive<Double>,
manufacturersIncludedFilterPressureDrop: Positive<Double>,
returnPlenumPressure: Positive<Double>,
postFilterPressure: Positive<Double>,
preCoilPressure: Positive<Double>,
supplyPlenumPressure: Positive<Double>
) {
self._airflow = airflow
self._manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop
self._returnPlenumPressure = returnPlenumPressure
self._postFilterPressure = postFilterPressure
self._preCoilPressure = preCoilPressure
self._supplyPlenumPressure = supplyPlenumPressure
}
public var externalStaticPressure: Positive<Double> {
($postFilterPressure - $manufacturersIncludedFilterPressureDrop) + $preCoilPressure
} }
} }
} }
//extension EquipmentMeasurement.AirHandler {
//
// public enum Key: String, Equatable, CaseIterable {
// case returnPlenumPressure
// case postFilterPressure
// case postCoilPressure
// case supplyPlenumPressure
// case airflow
// }
//}
//
//extension EquipmentMeasurement.FurnaceAndCoil {
//
// public enum Key: String, Equatable, CaseIterable {
// case returnPlenumPressure
// case postFilterPressure
// case preCoilPressure
// case supplyPlenumPressure
// case airflow
// }
//}
fileprivate extension Flagged { fileprivate extension Flagged {
init( init(
@@ -303,11 +329,11 @@ fileprivate extension Flagged {
} }
fileprivate func checkExternalStaticPressure( fileprivate func checkExternalStaticPressure(
value: Double, value: Positive<Double>,
ratedPressures: RatedStaticPressures ratedPressures: RatedStaticPressures
) -> Flagged { ) -> Flagged {
.init( .init(
wrappedValue: value, wrappedValue: value.positiveValue,
.rated(ratedPressures) .rated(ratedPressures)
) )
} }
@@ -330,6 +356,7 @@ extension EquipmentMeasurement {
case .airHandler: case .airHandler:
return .airHandler(.init( return .airHandler(.init(
airflow: 1200, airflow: 1200,
manufacturersIncludedFilterPressureDrop: 0.1,
returnPlenumPressure: 0.3, returnPlenumPressure: 0.3,
postFilterPressure: 0.6, postFilterPressure: 0.6,
postCoilPressure: 0.9, postCoilPressure: 0.9,
@@ -339,6 +366,7 @@ extension EquipmentMeasurement {
case .furnaceAndCoil: case .furnaceAndCoil:
return .furnaceAndCoil(.init( return .furnaceAndCoil(.init(
airflow: 1200, airflow: 1200,
manufacturersIncludedFilterPressureDrop: 0.0,
returnPlenumPressure: 0.3, returnPlenumPressure: 0.3,
postFilterPressure: 0.6, postFilterPressure: 0.6,
preCoilPressure: 0.4, preCoilPressure: 0.4,

View File

@@ -3,16 +3,16 @@ import Foundation
public struct EquipmentMetadata: Equatable { public struct EquipmentMetadata: Equatable {
public var coolingCapacity: CoolingCapacity public var coolingCapacity: CoolingCapacity
public var fanType: FanType public var fanType: FanType
public var ratedStaticPressure: RatedStaticPressures public var ratedStaticPressures: RatedStaticPressures
public init( public init(
coolingCapacity: CoolingCapacity = .three, coolingCapacity: CoolingCapacity = .three,
fanType: FanType = .constantSpeed, fanType: FanType = .constantSpeed,
ratedStaticPressure: RatedStaticPressures = .init() ratedStaticPressures: RatedStaticPressures = .init()
) { ) {
self.coolingCapacity = coolingCapacity self.coolingCapacity = coolingCapacity
self.fanType = fanType self.fanType = fanType
self.ratedStaticPressure = ratedStaticPressure self.ratedStaticPressures = ratedStaticPressures
} }
public enum CoolingCapacity: Double, Equatable, CaseIterable, Identifiable, CustomStringConvertible { public enum CoolingCapacity: Double, Equatable, CaseIterable, Identifiable, CustomStringConvertible {

View File

@@ -1,5 +1,8 @@
import Foundation import Foundation
/// Represents a number that can be checked if it is within an acceptable range. It can generate errors or warnings depending
/// on the current value.
///
@dynamicMemberLookup @dynamicMemberLookup
public struct Flagged: Equatable { public struct Flagged: Equatable {

View File

@@ -5,6 +5,10 @@ public struct Positive<Value> where Value: Numeric, Value: Comparable {
public var wrappedValue: Value public var wrappedValue: Value
public init(_ value: Value) {
self.wrappedValue = value
}
public init(wrappedValue: Value) { public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue self.wrappedValue = wrappedValue
} }

View File

@@ -10,9 +10,13 @@ extension View {
#endif #endif
} }
#warning("Fix me.")
// The decimal pad autocompletes too quickly in the simulator, needs tested on an actual
// device.
public func decimalPad() -> some View { public func decimalPad() -> some View {
#if os(iOS) #if os(iOS)
self.keyboardType(.decimalPad) // self.keyboardType(.decimalPad)
self.keyboardType(.numberPad)
#else #else
self self
#endif #endif

View File

@@ -88,6 +88,22 @@ extension DefaultNextButtonStyle where ButtonStyle == BorderedProminentButtonSty
} }
} }
public struct ToolbarNextButtonStyle: PrimitiveButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
Button(role: configuration.role, action: configuration.trigger) {
configuration.label
.foregroundStyle(Color.accentColor)
}
.labelStyle(ReverseLabelStyle())
.buttonStyle(.plain)
}
}
extension AnyPrimitiveButtonStyle<NextButtonType> {
public static var toolbar: Self { .init(ToolbarNextButtonStyle()) }
}
extension AnyButtonStyle where ButtonType == InfoButtonType { extension AnyButtonStyle where ButtonType == InfoButtonType {
public static var `default`: Self { public static var `default`: Self {
.init(DefaultInfoButtonStyle<IconOnlyLabelStyle>(labelStyle: .iconOnly)) .init(DefaultInfoButtonStyle<IconOnlyLabelStyle>(labelStyle: .iconOnly))