diff --git a/Package.swift b/Package.swift index cdcf32e..58db5ef 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,6 @@ let package = Package( products: [ .executable(name: "App", targets: ["App"]), .library(name: "ApiController", targets: ["ApiController"]), - .library(name: "ClimateZoneClient", targets: ["ClimateZoneClient"]), .library(name: "CoreModels", targets: ["CoreModels"]), .library(name: "LocationClient", targets: ["LocationClient"]), .library(name: "Routes", targets: ["Routes"]), @@ -62,22 +61,6 @@ let package = Package( ], 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( name: "CoreModels", dependencies: [], @@ -94,6 +77,7 @@ let package = Package( .target( name: "LocationClient", dependencies: [ + "CoreModels", .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies") ], diff --git a/Sources/ClimateZoneClient/ClimateZoneClient.swift b/Sources/ClimateZoneClient/ClimateZoneClient.swift deleted file mode 100644 index e69de29..0000000 diff --git a/Sources/CoreModels/ClimateZone.swift b/Sources/CoreModels/ClimateZone.swift index a29ac47..b66960b 100644 --- a/Sources/CoreModels/ClimateZone.swift +++ b/Sources/CoreModels/ClimateZone.swift @@ -1,6 +1,15 @@ 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 { // NOTE: Keep in this order. @@ -35,33 +44,33 @@ public enum ClimateZone { public var label: String { return "\(self == .hotHumid ? "Hot Humid" : rawValue.capitalized) (\(zoneIdentifiers.joined(separator: ", ")))" } + } - /// Represents climate zone identifiers. - public enum ZoneIdentifier: String, CaseIterable, Codable, Equatable, Sendable { - // A zones (hotHumid) - case oneA = "1A" - case twoA = "2A" - // A zones (moist) - case threeA = "3A" - case fourA = "4A" - case fiveA = "5A" - case sixA = "6A" - case sevenA = "7A" + /// Represents climate zone identifiers. + public enum ZoneIdentifier: String, CaseIterable, Codable, Equatable, Sendable { + // A zones (hotHumid) + case oneA = "1A" + case twoA = "2A" + // A zones (moist) + case threeA = "3A" + case fourA = "4A" + case fiveA = "5A" + case sixA = "6A" + case sevenA = "7A" - // B zones (dry) - case twoB = "2B" - case threeB = "3B" - case fourB = "4B" - case fiveB = "5B" - case sixB = "6B" - case sevenB = "7B" + // B zones (dry) + case twoB = "2B" + case threeB = "3B" + case fourB = "4B" + case fiveB = "5B" + case sixB = "6B" + case sevenB = "7B" - // C zones (marine) - case threeC = "3C" - case fourC = "4C" + // C zones (marine) + case threeC = "3C" + case fourC = "4C" - public var label: String { rawValue } + public var label: String { rawValue } - } } } diff --git a/Sources/LocationClient/LocationClient.swift b/Sources/LocationClient/LocationClient.swift index e0ab038..ce52938 100644 --- a/Sources/LocationClient/LocationClient.swift +++ b/Sources/LocationClient/LocationClient.swift @@ -1,3 +1,4 @@ +@_exported import CoreModels import Dependencies import DependenciesMacros import Foundation @@ -11,56 +12,132 @@ public extension DependencyValues { @DependencyClient public struct LocationClient: Sendable { - public var search: @Sendable (Int) async throws -> [Response] - // TODO: Add ClimateZone.ZoneIdentifier?? - public struct Response: Codable, Equatable, Sendable { + /// Estimate the climate zone by the heating design temperature and + /// the state code (ex. "OH"). + public var estimatedClimateZone: + @Sendable (_ heatingTemperature: Double, _ stateCode: String) async throws -> ClimateZone - public let city: String - public let latitude: String - public let longitude: String - public let zipCode: String - public let state: String - public let stateCode: String - public let county: String + /// Estimate the design temperatures based on location's coordinates. + public var estimatedDesignTemperatures: @Sendable (Coordinates) async throws -> DesignTemperatures - public init( - city: String, - latitude: String, - longitude: String, - zipCode: String, - state: String, - stateCode: String, - county: String - ) { - self.city = city - self.latitude = latitude - self.longitude = longitude - self.zipCode = zipCode - self.state = state - self.stateCode = stateCode - self.county = county - } + /// Get location details from a zip code. + public var search: @Sendable (_ zipCode: Int) async throws -> [Location] - private enum CodingKeys: String, CodingKey { - case city - case latitude - case longitude - case zipCode = "postal_code" - case state - case stateCode = "state_code" - case county = "province" - } + public func estimateDesignTemperaturesAndClimateZone( + coordinates: Coordinates, + stateCode: String + ) async throws -> (DesignTemperatures, ClimateZone) { + let designTemperatures = try await estimatedDesignTemperatures(coordinates) + let climateZone = try await estimatedClimateZone( + heatingTemperature: designTemperatures.heating, + stateCode: stateCode + ) + return (designTemperatures, climateZone) } + } +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 { + case city + case latitude + case longitude + case zipCode = "postal_code" + case state + case stateCode = "state_code" + 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 + } + + // Florida + 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 - public extension LocationClient.Response { + extension IntermediateResponse { static let mock = Self( city: "Monroe", latitude: "39.4413000", diff --git a/Sources/Routes/Models/HTMXExtensions.swift b/Sources/Routes/HTMXExtensions.swift similarity index 100% rename from Sources/Routes/Models/HTMXExtensions.swift rename to Sources/Routes/HTMXExtensions.swift diff --git a/Sources/Routes/Models/Capacitor.swift b/Sources/Routes/Models/CapacitorSizing.swift similarity index 100% rename from Sources/Routes/Models/Capacitor.swift rename to Sources/Routes/Models/CapacitorSizing.swift diff --git a/Sources/Routes/Models/HeatingBalancePoint.swift b/Sources/Routes/Models/HeatingBalancePoint.swift new file mode 100644 index 0000000..f207f0f --- /dev/null +++ b/Sources/Routes/Models/HeatingBalancePoint.swift @@ -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 + } + } + } +} diff --git a/Sources/Routes/SiteRoutes.swift b/Sources/Routes/SiteRoutes.swift index a0d6aec..7a3c0dd 100644 --- a/Sources/Routes/SiteRoutes.swift +++ b/Sources/Routes/SiteRoutes.swift @@ -4,6 +4,7 @@ import Foundation import PsychrometricClient @preconcurrency import URLRouting +// swiftlint:disable type_body_length public enum SiteRoute: Equatable, Sendable { case api(Api) @@ -392,3 +393,5 @@ public extension SiteRoute { } } } + +// swiftlint:enable type_body_length