π EnergyKit
κ°μ μ μλμ§ μ¬μ©μ μΆμ νκ³ μ΅μ ννλ νλ μμν¬
β¨ EnergyKitμ΄λ?
EnergyKitμ iOS 18μμ λμ λ νλ μμν¬λ‘, κ°μ μ μλμ§ μ¬μ©λμ μ€μκ°μΌλ‘ μΆμ νκ³ λΆμν©λλ€. HomeKitκ³Ό ν΅ν©λμ΄ μ€λ§νΈ νλ¬κ·Έ, μ λ ₯ μΈ‘μ κΈ°, νμκ΄ ν¨λ λ± μλμ§ κ΄λ ¨ κΈ°κΈ°μμ λ°μ΄ν°λ₯Ό μμ§νκ³ , μ¬μ©μμκ² μ λ ₯ μλΉ ν¨ν΄, μ¬μ μλμ§ μμ°λ, μ μ½ νμ μ 곡ν©λλ€. νκ²½ 보νΈμ μ κΈ° μκΈ μ κ°μ λμμ μ§μνλ μλμ§ κ΄λ¦¬ μ루μ μ λλ€.
π― 1. κΈ°λ³Έ μ€μ
EnergyKitμ μ΄κΈ°ννκ³ κΆνμ μμ²ν©λλ€.
import SwiftUI import HomeKit // μλμ§ κ΄λ¦¬μ @Observable class EnergyManager: NSObject { var homeManager = HMHomeManager() var currentHome: HMHome? var energyDevices: [EnergyDevice] = [] var isAuthorized = false override init() { super.init() homeManager.delegate = self } // HomeKit κΆν νμΈ func checkAuthorization() async -> Bool { // HomeKitμ μλμΌλ‘ κΆν μμ² await MainActor.run { isAuthorized = homeManager.authorizationStatus == .authorized } return isAuthorized } // μλμ§ κΈ°κΈ° κ²μ func discoverEnergyDevices() { guard let home = homeManager.primaryHome else { return } currentHome = home energyDevices = [] // μ λ ₯ μΈ‘μ κΈ°λ₯μ΄ μλ κΈ°κΈ° νν°λ§ for accessory in home.accessories { for service in accessory.services { // μ λ ₯ μΈ‘μ νΉμ± νμΈ if service.characteristics.contains(where: { $0.characteristicType == HMCharacteristicTypePowerState }) { let device = EnergyDevice( id: accessory.uniqueIdentifier, name: accessory.name, room: accessory.room?.name ?? "μ μ μμ", accessory: accessory ) energyDevices.append(device) } } } } } // HomeKit λΈλ¦¬κ²μ΄νΈ extension EnergyManager: HMHomeManagerDelegate { func homeManagerDidUpdateHomes(_ manager: HMHomeManager) { discoverEnergyDevices() } } // μλμ§ κΈ°κΈ° λͺ¨λΈ struct EnergyDevice: Identifiable { let id: UUID let name: String let room: String let accessory: HMAccessory var currentPower: Double = 0 // Watts var todayEnergy: Double = 0 // kWh }
β‘ 2. μ€μκ° μ λ ₯ λͺ¨λν°λ§
νμ¬ μ λ ₯ μλΉλμ μ€μκ°μΌλ‘ νμΈν©λλ€.
import SwiftUI import Charts struct PowerReading: Identifiable { let id = UUID() let timestamp: Date let power: Double // Watts } @Observable class PowerMonitor { var currentPower: Double = 0 var readings: [PowerReading] = [] private var timer: Timer? // μ€μκ° λͺ¨λν°λ§ μμ func startMonitoring() { timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in Task { @MainActor in await self.updatePowerReading() } } } // λͺ¨λν°λ§ μ€μ§ func stopMonitoring() { timer?.invalidate() timer = nil } // μ λ ₯ μΈ‘μ func updatePowerReading() async { // μ€μ λ‘λ HomeKitμμ λ°μ΄ν° μ½κΈ° let power = Double.random(in: 500...2000) currentPower = power readings.append(PowerReading(timestamp: Date(), power: power)) // μ΅κ·Ό 1μκ° λ°μ΄ν°λ§ μ μ§ let oneHourAgo = Date().addingTimeInterval(-3600) readings.removeAll { $0.timestamp < oneHourAgo } } var averagePower: Double { guard !readings.isEmpty else { return 0 } return readings.map { $0.power }.reduce(0, +) / Double(readings.count) } } struct RealTimePowerView: View { @State private var monitor = PowerMonitor() var body: some View { ScrollView { VStack(spacing: 24) { // νμ¬ μ λ ₯ VStack { Text("νμ¬ μ¬μ©λ") .font(.headline) .foregroundStyle(.secondary) HStack(alignment: .firstTextBaseline, spacing: 4) { Text("\(Int(monitor.currentPower))") .font(.system(size: 60, weight: .bold)) .contentTransition(.numericText()) Text("W") .font(.title) .foregroundStyle(.secondary) } Text("νκ· \(Int(monitor.averagePower))W") .font(.subheadline) .foregroundStyle(.secondary) } .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(16) // μ€μκ° μ°¨νΈ VStack(alignment: .leading) { Text("μ΅κ·Ό 1μκ°") .font(.headline) Chart(monitor.readings) { reading in LineMark( x: .value("μκ°", reading.timestamp), y: .value("μ λ ₯", reading.power) ) .foregroundStyle(.blue) .interpolationMethod(.catmullRom) AreaMark( x: .value("μκ°", reading.timestamp), y: .value("μ λ ₯", reading.power) ) .foregroundStyle(.blue.opacity(0.2)) .interpolationMethod(.catmullRom) } .frame(height: 200) .chartYAxis { AxisMarks { value in AxisValueLabel { if let power = value.as(Double.self) { Text("\(Int(power))W") } } } } } // λΉμ© μΆμ HStack { VStack(alignment: .leading) { Text("μκ°λΉ λΉμ©") .font(.caption) .foregroundStyle(.secondary) Text("\(estimatedHourlyCost)μ") .font(.title3) .fontWeight(.semibold) } Spacer() VStack(alignment: .trailing) { Text("μ μμ λΉμ©") .font(.caption) .foregroundStyle(.secondary) Text("\(estimatedMonthlyCost)μ") .font(.title3) .fontWeight(.semibold) } } .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(12) } .padding() } .navigationTitle("μ λ ₯ λͺ¨λν°") .onAppear { monitor.startMonitoring() } .onDisappear { monitor.stopMonitoring() } } // μ κΈ° μκΈ κ³μ° (kWhλΉ 300μ κ°μ ) var estimatedHourlyCost: Int { Int(monitor.currentPower / 1000 * 300) } var estimatedMonthlyCost: Int { estimatedHourlyCost * 24 * 30 } }
π 3. κΈ°κΈ°λ³ μ¬μ©λ λΆμ
κ° κΈ°κΈ°μ μλμ§ μλΉλ₯Ό λΆμνκ³ λΉκ΅ν©λλ€.
import SwiftUI import Charts struct DeviceEnergyData: Identifiable { let id = UUID() let deviceName: String let energy: Double // kWh let percentage: Double let category: DeviceCategory } enum DeviceCategory: String, CaseIterable { case lighting = "μ‘°λͺ " case hvac = "λλλ°©" case appliance = "κ°μ μ ν" case entertainment = "μν°ν μΈλ¨ΌνΈ" case other = "κΈ°ν" var color: Color { switch self { case .lighting: return .yellow case .hvac: return .blue case .appliance: return .green case .entertainment: return .purple case .other: return .gray } } } struct DeviceAnalysisView: View { @State private var devices: [DeviceEnergyData] = [ .init(deviceName: "κ±°μ€ μμ΄μ»¨", energy: 45.2, percentage: 35, category: .hvac), .init(deviceName: "λμ₯κ³ ", energy: 28.5, percentage: 22, category: .appliance), .init(deviceName: "μ‘°λͺ ", energy: 18.7, percentage: 15, category: .lighting), .init(deviceName: "TV", energy: 15.3, percentage: 12, category: .entertainment), .init(deviceName: "μΈνκΈ°", energy: 12.8, percentage: 10, category: .appliance), .init(deviceName: "κΈ°ν", energy: 8.5, percentage: 6, category: .other) ] var totalEnergy: Double { devices.map { $0.energy }.reduce(0, +) } var body: some View { ScrollView { VStack(spacing: 24) { // μ΄ μ¬μ©λ VStack { Text("μ΄λ² λ¬ μ΄ μ¬μ©λ") .font(.headline) .foregroundStyle(.secondary) HStack(alignment: .firstTextBaseline) { Text("\(totalEnergy, specifier: "%.1f")") .font(.system(size: 48, weight: .bold)) Text("kWh") .font(.title2) .foregroundStyle(.secondary) } } // μν μ°¨νΈ Chart(devices) { device in SectorMark( angle: .value("μλμ§", device.energy), innerRadius: .ratio(0.6), angularInset: 2 ) .foregroundStyle(device.category.color) .annotation(position: .overlay) { Text("\(Int(device.percentage))%") .font(.caption) .fontWeight(.bold) } } .frame(height: 250) // κΈ°κΈ° λͺ©λ‘ VStack(alignment: .leading, spacing: 12) { Text("κΈ°κΈ°λ³ μμΈ") .font(.headline) ForEach(devices) { device in HStack { Circle() .fill(device.category.color) .frame(width: 12, height: 12) VStack(alignment: .leading, spacing: 2) { Text(device.deviceName) .font(.subheadline) Text(device.category.rawValue) .font(.caption) .foregroundStyle(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 2) { Text("\(device.energy, specifier: "%.1f") kWh") .font(.subheadline) .fontWeight(.semibold) Text("\(Int(device.percentage))%") .font(.caption) .foregroundStyle(.secondary) } } .padding() .background(Color.gray.opacity(0.05)) .cornerRadius(8) } } // μ μ½ ν VStack(alignment: .leading, spacing: 12) { Label("μ μ½ ν", systemImage: "lightbulb.fill") .font(.headline) .foregroundStyle(.orange) Text("μμ΄μ»¨ μ€μ μ¨λλ₯Ό 1λ μ¬λ¦¬λ©΄ μ μ½ 6% μ μ½ν μ μμ΅λλ€.") .font(.subheadline) } .padding() .background(Color.orange.opacity(0.1)) .cornerRadius(12) } .padding() } .navigationTitle("κΈ°κΈ°λ³ μ¬μ©λ") } }
βοΈ 4. μ¬μ μλμ§ λͺ¨λν°λ§
νμκ΄ ν¨λ λ± μ¬μ μλμ§ μμ°λμ μΆμ ν©λλ€.
import SwiftUI import Charts struct SolarData: Identifiable { let id = UUID() let hour: Int let production: Double // kWh let consumption: Double // kWh } @Observable class SolarEnergyManager { var todayProduction: Double = 18.5 var todayConsumption: Double = 22.3 var gridExport: Double = 3.2 // νλ§€λ var gridImport: Double = 7.0 // ꡬ맀λ var hourlyData: [SolarData] = [ .init(hour: 6, production: 0.2, consumption: 0.8), .init(hour: 7, production: 0.8, consumption: 1.2), .init(hour: 8, production: 1.5, consumption: 1.0), .init(hour: 9, production: 2.3, consumption: 0.9), .init(hour: 10, production: 3.0, consumption: 0.8), .init(hour: 11, production: 3.5, consumption: 1.0), .init(hour: 12, production: 3.8, consumption: 1.2), .init(hour: 13, production: 3.4, consumption: 1.5), .init(hour: 14, production: 2.8, consumption: 1.3), .init(hour: 15, production: 2.0, consumption: 1.8), .init(hour: 16, production: 1.2, consumption: 2.5), .init(hour: 17, production: 0.5, consumption: 3.2) ] var selfSufficiencyRate: Double { (todayProduction / todayConsumption) * 100 } } struct SolarEnergyView: View { @State private var manager = SolarEnergyManager() var body: some View { ScrollView { VStack(spacing: 24) { // μ€λ μμ½ HStack(spacing: 12) { VStack { Image(systemName: "sun.max.fill") .font(.title) .foregroundStyle(.orange) Text("μμ°") .font(.caption) .foregroundStyle(.secondary) Text("\(manager.todayProduction, specifier: "%.1f") kWh") .font(.headline) } .frame(maxWidth: .infinity) .padding() .background(Color.orange.opacity(0.1)) .cornerRadius(12) VStack { Image(systemName: "bolt.fill") .font(.title) .foregroundStyle(.blue) Text("μ¬μ©") .font(.caption) .foregroundStyle(.secondary) Text("\(manager.todayConsumption, specifier: "%.1f") kWh") .font(.headline) } .frame(maxWidth: .infinity) .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(12) } // μκΈλ₯ VStack { Text("μλμ§ μκΈλ₯ ") .font(.headline) ZStack { Circle() .stroke(Color.gray.opacity(0.2), lineWidth: 20) Circle() .trim(from: 0, to: manager.selfSufficiencyRate / 100) .stroke( LinearGradient( colors: [.green, .yellow], startPoint: .leading, endPoint: .trailing ), style: StrokeStyle(lineWidth: 20, lineCap: .round) ) .rotationEffect(.degrees(-90)) .animation(.easeInOut, value: manager.selfSufficiencyRate) VStack { Text("\(Int(manager.selfSufficiencyRate))%") .font(.system(size: 42, weight: .bold)) Text("μκΈλ₯ ") .font(.caption) .foregroundStyle(.secondary) } } .frame(width: 200, height: 200) } // μκ°λ³ μ°¨νΈ VStack(alignment: .leading) { Text("μκ°λ³ μμ°/μλΉ") .font(.headline) Chart { ForEach(manager.hourlyData) { data in // μμ°λ BarMark( x: .value("μκ°", data.hour), y: .value("μμ°", data.production) ) .foregroundStyle(.orange) .position(by: .value("νμ ", "μμ°")) // μλΉλ BarMark( x: .value("μκ°", data.hour), y: .value("μλΉ", data.consumption) ) .foregroundStyle(.blue) .position(by: .value("νμ ", "μλΉ")) } } .frame(height: 200) .chartXAxis { AxisMarks { value in AxisValueLabel { if let hour = value.as(Int.self) { Text("\(hour)μ") } } } } } // μ λ ₯λ§ κ±°λ VStack(alignment: .leading, spacing: 12) { Text("μ λ ₯λ§ κ±°λ") .font(.headline) HStack { Label("νλ§€", systemImage: "arrow.up.circle.fill") .foregroundStyle(.green) Spacer() Text("\(manager.gridExport, specifier: "%.1f") kWh") .fontWeight(.semibold) } HStack { Label("ꡬ맀", systemImage: "arrow.down.circle.fill") .foregroundStyle(.red) Spacer() Text("\(manager.gridImport, specifier: "%.1f") kWh") .fontWeight(.semibold) } } .padding() .background(Color.gray.opacity(0.05)) .cornerRadius(12) // νκ²½ μν₯ VStack(alignment: .leading, spacing: 8) { Label("νκ²½ κΈ°μ¬λ", systemImage: "leaf.fill") .font(.headline) .foregroundStyle(.green) Text("μ€λ \(carbonReduction)kgμ COβ λ°°μΆμ μ€μμ΅λλ€") .font(.subheadline) Text("λ무 \(treesEquivalent)그루 μ¬μ ν¨κ³Ό") .font(.caption) .foregroundStyle(.secondary) } .padding() .background(Color.green.opacity(0.1)) .cornerRadius(12) } .padding() } .navigationTitle("νμκ΄ μλμ§") } // CO2 μ κ°λ (kWhλΉ μ½ 0.46kg) var carbonReduction: Double { manager.todayProduction * 0.46 } // λ무 νμ° (λ무 1그루λ μ°κ° μ½ 6kg CO2 ν‘μ) var treesEquivalent: Int { Int(carbonReduction / 6 * 365) } }
π 5. μ¬μ© ν¨ν΄ λΆμ
μλμ§ μ¬μ© ν¨ν΄μ λΆμνκ³ μ΅μ ν μ μμ μ 곡ν©λλ€.
import SwiftUI import Charts struct DailyUsage: Identifiable { let id = UUID() let date: Date let energy: Double let cost: Double } @Observable class UsagePatternAnalyzer { var weeklyData: [DailyUsage] = [] var peakHour: Int = 19 // μ€ν 7μ var recommendations: [String] = [] init() { generateWeeklyData() analyzePattern() } func generateWeeklyData() { let calendar = Calendar.current for day in 0.<7 { let date = calendar.date(byAdding: .day, value: -day, to: Date())! let energy = Double.random(in: 15...30) let cost = energy * 300 // kWhλΉ 300μ weeklyData.insert( DailyUsage(date: date, energy: energy, cost: cost), at: 0 ) } } func analyzePattern() { // ν¨ν΄ λΆμ λ° μΆμ² recommendations = [ "μ€ν 7-9μμ μ¬μ©λμ΄ κ°μ₯ λμ΅λλ€. μΈνκΈ°λ μ¬μΌ μκ°μ μ¬μ©νμΈμ.", "νκ· λ³΄λ€ 20% λ§μ΄ μ¬μ©νκ³ μμ΅λλ€. λκΈ°μ λ ₯μ μ°¨λ¨νμΈμ.", "λλλ°© μ€μ μ¨λλ₯Ό μ‘°μ νλ©΄ μ 15,000μ μ μ½ν μ μμ΅λλ€." ] } var averageDaily: Double { weeklyData.map { $0.energy }.reduce(0, +) / Double(weeklyData.count) } var estimatedMonthlyCost: Double { averageDaily * 30 * 300 } } struct UsagePatternView: View { @State private var analyzer = UsagePatternAnalyzer() var body: some View { ScrollView { VStack(spacing: 24) { // μ£Όκ° μΆμ΄ VStack(alignment: .leading) { Text("μ£Όκ° μ¬μ© μΆμ΄") .font(.headline) Chart(analyzer.weeklyData) { usage in BarMark( x: .value("λ μ§", usage.date, unit: .day), y: .value("μλμ§", usage.energy) ) .foregroundStyle( LinearGradient( colors: [.blue, .cyan], startPoint: .top, endPoint: .bottom ) ) RuleMark(y: .value("νκ· ", analyzer.averageDaily)) .foregroundStyle(.red) .lineStyle(StrokeStyle(lineWidth: 2, dash: [5])) .annotation(position: .top, alignment: .trailing) { Text("νκ· ") .font(.caption) .foregroundStyle(.red) } } .frame(height: 200) .chartXAxis { AxisMarks { value in AxisValueLabel(format: .dateTime.weekday(.narrow)) } } } // ν΅κ³ HStack(spacing: 12) { VStack { Text("μΌνκ· ") .font(.caption) .foregroundStyle(.secondary) Text("\(analyzer.averageDaily, specifier: "%.1f")") .font(.title2) .fontWeight(.bold) Text("kWh") .font(.caption) } .frame(maxWidth: .infinity) .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(12) VStack { Text("μμμ") .font(.caption) .foregroundStyle(.secondary) Text("\(Int(analyzer.estimatedMonthlyCost))") .font(.title2) .fontWeight(.bold) Text("μ") .font(.caption) } .frame(maxWidth: .infinity) .padding() .background(Color.green.opacity(0.1)) .cornerRadius(12) } // νΌν¬ νμ VStack(alignment: .leading, spacing: 8) { Label("μ΅λ μ¬μ© μκ°", systemImage: "clock.fill") .font(.headline) Text("μ€ν \(analyzer.peakHour - 12)μ") .font(.title3) .fontWeight(.semibold) .foregroundStyle(.blue) Text("μ κΈ° μκΈμ΄ μ λ ΄ν μ¬μΌ μκ°(μ€ν 11μ~μ€μ 9μ)μ κ³ μ λ ₯ κΈ°κΈ°λ₯Ό μ¬μ©νμΈμ.") .font(.caption) .foregroundStyle(.secondary) } .padding() .background(Color.gray.opacity(0.05)) .cornerRadius(12) // μΆμ² μ¬ν VStack(alignment: .leading, spacing: 16) { Label("λ§μΆ€ μΆμ²", systemImage: "sparkles") .font(.headline) ForEach(Array(analyzer.recommendations.enumerated()), id: \.0) { index, tip in HStack(alignment: .top, spacing: 12) { Text("\(index + 1)") .font(.caption) .fontWeight(.bold) .foregroundStyle(.white) .frame(width: 24, height: 24) .background(Color.blue) .clipShape(Circle()) Text(tip) .font(.subheadline) } } } .padding() .background(Color.blue.opacity(0.05)) .cornerRadius(12) } .padding() } .navigationTitle("μ¬μ© ν¨ν΄ λΆμ") } }
π‘ HIG κ°μ΄λλΌμΈ
- νλΌμ΄λ²μ: μλμ§ λ°μ΄ν°λ λ―Όκ° μ 보μ΄λ―λ‘ μ¨λλ°μ΄μ€μμ μ²λ¦¬
- μ€μκ° μ λ°μ΄νΈ: νμ¬ μ λ ₯ μλΉλ₯Ό 5-10μ΄ κ°κ²©μΌλ‘ μ λ°μ΄νΈ
- λͺ νν μκ°ν: μ°¨νΈμ κ·Έλνλ‘ μ΄ν΄νκΈ° μ½κ² νμ
- μ€μ©μ μΈ ν: μ€μ λ‘ μ€μ² κ°λ₯ν μ μ½ λ°©λ² μ μ
- HomeKit ν΅ν©: κΈ°μ‘΄ HomeKit κΈ°κΈ°μ μννκ² μ°λ
π― μ€μ νμ©
- μ€λ§νΈν μ±: μ 체 κ°μ μ μλμ§ λμ보λ
- νμκ΄ λͺ¨λν°λ§: μ¬μ μλμ§ μμ°λ μΆμ
- μλμ§ μ μ½ μ±λ¦°μ§: κ²μ΄λ―ΈνΌμΌμ΄μ μΌλ‘ μ μ½ μ λ
- λΆλμ° κ΄λ¦¬: 건물 μλμ§ ν¨μ¨ λͺ¨λν°λ§
- νκ²½ μ±: νμ λ°μκ΅ κ³μ° λ° κ°μ