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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import ComposableArchitecture
import DependenciesAdditions
import SharedModels
import Styleguide
import SwiftUI
import TCAExtras
#warning("Fix shared settings.")
@Reducer
public struct PressureEstimationsFeature {
@@ -17,17 +19,16 @@ public struct PressureEstimationsFeature {
@ObservableState
public struct State: Equatable {
@Presents public var destination: Destination.State?
@Shared(.sharedPressureEstimationSettings) var sharedSettings = SharedPressureEstimationSettings()
public var equipmentSettings: EquipmentSettingsForm.State
public var equipmentMeasurements: EquipmentMeasurementForm.State?
public init(
destination: Destination.State? = nil,
equipmentSettings: EquipmentSettingsForm.State = .init(),
equipmentMeasurements: EquipmentMeasurementForm.State? = nil
sharedSettings: SharedPressureEstimationSettings = .init()
) {
self.destination = destination
self.equipmentSettings = equipmentSettings
self.equipmentMeasurements = equipmentMeasurements
self._sharedSettings = Shared(sharedSettings)
self._equipmentSettings = .init(sharedSettings: self._sharedSettings)
}
}
@@ -41,7 +42,9 @@ public struct PressureEstimationsFeature {
case nextButtonTapped
}
}
@Dependency(\.logger["\(Self.self)"]) var logger
public var body: some Reducer<State, Action> {
Scope(state: \.equipmentSettings, action: \.equipmentSettings) {
EquipmentSettingsForm()
@@ -57,13 +60,42 @@ public struct PressureEstimationsFeature {
case let .view(action):
switch action {
case .nextButtonTapped:
return .none
return handleNextButtonTapped(&state)
}
}
}
.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)
@@ -79,5 +111,30 @@ public struct PressureEstimationsView: View {
EquipmentSettingsFormView(
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
#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 {
case airHandler(AirHandler)
@@ -17,7 +14,7 @@ public enum EquipmentMeasurement: Equatable {
}
}
public var externalStaticPressure: Double {
public var externalStaticPressure: Positive<Double> {
switch self {
case let .airHandler(airHandler):
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 {
@Positive
public var airflow: Double?
public var airflow: Double
@Positive
public var returnPlenumPressure: Double?
public var manufacturersIncludedFilterPressureDrop: Double
@Positive
public var returnPlenumPressure: Double
@Positive
public var postFilterPressure: Double?
public var postFilterPressure: Double
@Positive
public var postCoilPressure: Double?
public var postCoilPressure: Double
@Positive
public var supplyPlenumPressure: Double?
public var supplyPlenumPressure: Double
public init(
airflow: Double? = nil,
returnPlenumPressure: Double? = nil,
postFilterPressure: Double? = nil,
postCoilPressure: Double? = nil,
supplyPlenumPressure: Double? = nil
airflow: Double,
manufacturersIncludedFilterPressureDrop: Double,
returnPlenumPressure: Double,
postFilterPressure: Double,
postCoilPressure: Double,
supplyPlenumPressure: Double
) {
self.airflow = airflow
self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop
self.returnPlenumPressure = returnPlenumPressure
self.postFilterPressure = postFilterPressure
self.postCoilPressure = postCoilPressure
self.supplyPlenumPressure = supplyPlenumPressure
}
public var externalStaticPressure: Double {
var postFilterAdder = 0.0
if let postFilterPressure = $postFilterPressure.positiveValue,
postFilterPressure > 0.1
{
postFilterAdder = postFilterPressure - 0.1
public init(
airflow: Positive<Double>,
manufacturersIncludedFilterPressureDrop: Positive<Double>,
returnPlenumPressure: Positive<Double>,
postFilterPressure: Positive<Double>,
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
+ ($supplyPlenumPressure.positiveValue ?? 0)
+ $supplyPlenumPressure
}
}
@@ -145,7 +170,7 @@ public enum EquipmentMeasurement: Equatable {
.init(
airflow: checkAirflow(value: measurement.airflow, tons: tons),
coilPressureDrop: .init(
wrappedValue: (measurement.$postCoilPressure.positiveValue - measurement.$postFilterPressure.positiveValue) ?? 0,
wrappedValue: (measurement.$postCoilPressure.positiveValue - measurement.$postFilterPressure.positiveValue),
.result(.good())
),
externalStaticPressure: checkExternalStaticPressure(
@@ -155,7 +180,8 @@ public enum EquipmentMeasurement: Equatable {
filterPressureDrop: .init(
value: measurement.$postFilterPressure.positiveValue - measurement.$returnPlenumPressure.positiveValue,
budget: budgets.filterBudget,
ratedPressures: ratedPressures
ratedPressures: ratedPressures,
ignoreMinimum: true
),
returnPlenumPressure: .init(
value: measurement.$returnPlenumPressure.positiveValue,
@@ -163,7 +189,7 @@ public enum EquipmentMeasurement: Equatable {
ratedPressures: ratedPressures
),
supplyPlenumPressure: .init(
value: measurement.$supplyPlenumPressure.positiveValue ?? 0,
value: measurement.$supplyPlenumPressure.positiveValue,
budget: budgets.supplyPlenumBudget,
ratedPressures: ratedPressures
)
@@ -211,61 +237,61 @@ public enum EquipmentMeasurement: Equatable {
public struct FurnaceAndCoil: Equatable {
@Positive
public var airflow: Double?
public var airflow: Double
@Positive
public var returnPlenumPressure: Double?
public var manufacturersIncludedFilterPressureDrop: Double
@Positive
public var returnPlenumPressure: Double
@Positive
public var postFilterPressure: Double?
public var postFilterPressure: Double
@Positive
public var preCoilPressure: Double?
public var preCoilPressure: Double
@Positive
public var supplyPlenumPressure: Double?
public var supplyPlenumPressure: Double
public init(
airflow: Double? = nil,
returnPlenumPressure: Double? = nil,
postFilterPressure: Double? = nil,
preCoilPressure: Double? = nil,
supplyPlenumPressure: Double? = nil
airflow: Double,
manufacturersIncludedFilterPressureDrop: Double,
returnPlenumPressure: Double,
postFilterPressure: Double,
preCoilPressure: Double,
supplyPlenumPressure: Double
) {
self.airflow = airflow
self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop
self.returnPlenumPressure = returnPlenumPressure
self.postFilterPressure = postFilterPressure
self.preCoilPressure = preCoilPressure
self.supplyPlenumPressure = supplyPlenumPressure
}
public var externalStaticPressure: Double {
($postFilterPressure.positiveValue ?? 0) + ($preCoilPressure.positiveValue ?? 0)
public init(
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 {
init(
@@ -303,11 +329,11 @@ fileprivate extension Flagged {
}
fileprivate func checkExternalStaticPressure(
value: Double,
value: Positive<Double>,
ratedPressures: RatedStaticPressures
) -> Flagged {
.init(
wrappedValue: value,
wrappedValue: value.positiveValue,
.rated(ratedPressures)
)
}
@@ -330,6 +356,7 @@ extension EquipmentMeasurement {
case .airHandler:
return .airHandler(.init(
airflow: 1200,
manufacturersIncludedFilterPressureDrop: 0.1,
returnPlenumPressure: 0.3,
postFilterPressure: 0.6,
postCoilPressure: 0.9,
@@ -339,6 +366,7 @@ extension EquipmentMeasurement {
case .furnaceAndCoil:
return .furnaceAndCoil(.init(
airflow: 1200,
manufacturersIncludedFilterPressureDrop: 0.0,
returnPlenumPressure: 0.3,
postFilterPressure: 0.6,
preCoilPressure: 0.4,

View File

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

View File

@@ -1,5 +1,8 @@
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
public struct Flagged: Equatable {

View File

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

View File

@@ -10,9 +10,13 @@ extension View {
#endif
}
#warning("Fix me.")
// The decimal pad autocompletes too quickly in the simulator, needs tested on an actual
// device.
public func decimalPad() -> some View {
#if os(iOS)
self.keyboardType(.decimalPad)
// self.keyboardType(.decimalPad)
self.keyboardType(.numberPad)
#else
self
#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 {
public static var `default`: Self {
.init(DefaultInfoButtonStyle<IconOnlyLabelStyle>(labelStyle: .iconOnly))