feat: WIP

This commit is contained in:
2025-03-04 07:26:51 -05:00
parent 9da4149391
commit 19a51598e4
8 changed files with 201 additions and 78 deletions

View File

@@ -9,7 +9,6 @@ let package = Package(
products: [ products: [
.executable(name: "App", targets: ["App"]), .executable(name: "App", targets: ["App"]),
.library(name: "ApiController", targets: ["ApiController"]), .library(name: "ApiController", targets: ["ApiController"]),
.library(name: "ClimateZoneClient", targets: ["ClimateZoneClient"]),
.library(name: "CoreModels", targets: ["CoreModels"]), .library(name: "CoreModels", targets: ["CoreModels"]),
.library(name: "LocationClient", targets: ["LocationClient"]), .library(name: "LocationClient", targets: ["LocationClient"]),
.library(name: "Routes", targets: ["Routes"]), .library(name: "Routes", targets: ["Routes"]),
@@ -62,22 +61,6 @@ let package = Package(
], ],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.target(
name: "LocationClient",
dependencies: [
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies")
],
swiftSettings: swiftSettings
),
.target(
name: "ClimateZoneClient",
dependencies: [
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies")
],
swiftSettings: swiftSettings
),
.target( .target(
name: "CoreModels", name: "CoreModels",
dependencies: [], dependencies: [],
@@ -94,6 +77,7 @@ let package = Package(
.target( .target(
name: "LocationClient", name: "LocationClient",
dependencies: [ dependencies: [
"CoreModels",
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies") .product(name: "DependenciesMacros", package: "swift-dependencies")
], ],

View File

@@ -1,6 +1,15 @@
import Foundation import Foundation
public enum ClimateZone { public enum ClimateZone: String, CaseIterable, Codable, Equatable, Sendable {
case one = "CZ1"
case two = "CZ2"
case three = "CZ3"
case four = "CZ4"
case five = "CZ5"
case six = "CZ6"
public var label: String { rawValue }
public enum ZoneType: String, CaseIterable, Codable, Equatable, Sendable { public enum ZoneType: String, CaseIterable, Codable, Equatable, Sendable {
// NOTE: Keep in this order. // NOTE: Keep in this order.
@@ -35,6 +44,7 @@ public enum ClimateZone {
public var label: String { public var label: String {
return "\(self == .hotHumid ? "Hot Humid" : rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ", ")))" return "\(self == .hotHumid ? "Hot Humid" : rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ", ")))"
} }
}
/// Represents climate zone identifiers. /// Represents climate zone identifiers.
public enum ZoneIdentifier: String, CaseIterable, Codable, Equatable, Sendable { public enum ZoneIdentifier: String, CaseIterable, Codable, Equatable, Sendable {
@@ -64,4 +74,3 @@ public enum ClimateZone {
} }
} }
}

View File

@@ -1,3 +1,4 @@
@_exported import CoreModels
import Dependencies import Dependencies
import DependenciesMacros import DependenciesMacros
import Foundation import Foundation
@@ -11,35 +12,61 @@ public extension DependencyValues {
@DependencyClient @DependencyClient
public struct LocationClient: Sendable { public struct LocationClient: Sendable {
public var search: @Sendable (Int) async throws -> [Response]
// TODO: Add ClimateZone.ZoneIdentifier?? /// Estimate the climate zone by the heating design temperature and
public struct Response: Codable, Equatable, Sendable { /// the state code (ex. "OH").
public var estimatedClimateZone:
@Sendable (_ heatingTemperature: Double, _ stateCode: String) async throws -> ClimateZone
public let city: String /// Estimate the design temperatures based on location's coordinates.
public let latitude: String public var estimatedDesignTemperatures: @Sendable (Coordinates) async throws -> DesignTemperatures
public let longitude: String
public let zipCode: String
public let state: String
public let stateCode: String
public let county: String
public init( /// Get location details from a zip code.
city: String, public var search: @Sendable (_ zipCode: Int) async throws -> [Location]
latitude: String,
longitude: String, public func estimateDesignTemperaturesAndClimateZone(
zipCode: String, coordinates: Coordinates,
state: String, stateCode: String
stateCode: String, ) async throws -> (DesignTemperatures, ClimateZone) {
county: String let designTemperatures = try await estimatedDesignTemperatures(coordinates)
) { let climateZone = try await estimatedClimateZone(
self.city = city heatingTemperature: designTemperatures.heating,
self.latitude = latitude stateCode: stateCode
self.longitude = longitude )
self.zipCode = zipCode return (designTemperatures, climateZone)
self.state = state }
self.stateCode = stateCode
self.county = county }
struct DecodingError: Error {}
extension LocationClient: TestDependencyKey {
public static let testValue: LocationClient = Self()
}
// Intermediate response returned from 'https://app.zipcodebase.com'
private struct IntermediateResponse: Codable, Equatable, Sendable {
let city: String
let latitude: String
let longitude: String
let zipCode: String
let state: String
let stateCode: String
let county: String
func toLocation() throws -> Location {
guard let longitude = Double(longitude), let latitude = Double(latitude) else {
throw DecodingError()
}
return .init(
city: city,
state: state,
stateCode: stateCode,
zipCode: zipCode,
county: county,
coordinates: .init(latitude: latitude, longitude: longitude)
)
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
@@ -52,15 +79,65 @@ public struct LocationClient: Sendable {
case county = "province" case county = "province"
} }
} }
private func estimatedHeatingTemperature(_ coordinates: Coordinates) -> Double {
let latitude = coordinates.latitude
let longitude = coordinates.longitude
// Gulf coast region
if latitude < 31, longitude > -95 && longitude < -89 {
return 35
} }
extension LocationClient: TestDependencyKey { // Florida
public static let testValue: LocationClient = Self() if latitude < 31, longitude > -87.5 && longitude < -80 {
return 40
}
let absLat = abs(latitude)
if absLat < 30 { return 35 }
if absLat < 33 { return 30 }
if absLat < 36 { return 25 }
if absLat < 40 { return 15 }
if absLat < 45 { return 5 }
return 0
}
private func estimatedCoolingTemperature(_ coordinates: Coordinates) -> Double {
let latitude = coordinates.latitude
let longitude = coordinates.longitude
// Gulf coast and Florida
if latitude < 31, longitude > -95 && longitude < -80 {
return 95
}
let absLat = abs(latitude)
if absLat < 30 { return 95 }
if absLat < 33 { return 92 }
if absLat < 36 { return 90 }
if absLat < 40 { return 90 }
if absLat < 45 { return 88 }
return 85
}
private func determineClimateZone(heatingTemperature: Double, stateCode: String) -> ClimateZone {
let hotHumidStates = ["FL", "LA", "TX", "MS", "AL", "GA", "SC"]
if hotHumidStates.contains(stateCode.uppercased()) {
return .two
}
if heatingTemperature >= 45 { return .one }
if heatingTemperature >= 35 { return .two }
if heatingTemperature >= 25 { return .three }
if heatingTemperature >= 15 { return .four }
if heatingTemperature >= 5 { return .five }
return .six
} }
#if DEBUG #if DEBUG
public extension LocationClient.Response { extension IntermediateResponse {
static let mock = Self( static let mock = Self(
city: "Monroe", city: "Monroe",
latitude: "39.4413000", latitude: "39.4413000",

View File

@@ -0,0 +1,50 @@
public enum HeatingBalancePoint {
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
case thermal
}
public enum Request: Codable, Equatable, Sendable {
case thermal(Thermal)
public struct Thermal: Codable, Equatable, Sendable {
public let systemSize: Double
public let capacityAt47: Double?
public let capacityAt17: Double?
public let heatingDesignTemperature: Double
public let buildingHeatLoss: Double
public init(
systemSize: Double,
capacityAt47: Double? = nil,
capacityAt17: Double? = nil,
heatingDesignTemperature: Double,
buildingHeatLoss: Double
) {
self.systemSize = systemSize
self.capacityAt47 = capacityAt47
self.capacityAt17 = capacityAt17
self.heatingDesignTemperature = heatingDesignTemperature
self.buildingHeatLoss = buildingHeatLoss
}
}
}
public enum Response: Codable, Equatable, Sendable {
case thermal(Thermal)
public struct Thermal: Codable, Equatable, Sendable {
public let capacityAt47: Double
public let capacityAt17: Double
public let balancePointTemperature: Double
public init(capacityAt47: Double, capacityAt17: Double, balancePointTemperature: Double) {
self.capacityAt47 = capacityAt47
self.capacityAt17 = capacityAt17
self.balancePointTemperature = balancePointTemperature
}
}
}
}

View File

@@ -4,6 +4,7 @@ import Foundation
import PsychrometricClient import PsychrometricClient
@preconcurrency import URLRouting @preconcurrency import URLRouting
// swiftlint:disable type_body_length
public enum SiteRoute: Equatable, Sendable { public enum SiteRoute: Equatable, Sendable {
case api(Api) case api(Api)
@@ -392,3 +393,5 @@ public extension SiteRoute {
} }
} }
} }
// swiftlint:enable type_body_length